Skip to content

Commit 7050832

Browse files
dginevclaude
andauthored
fix alignment-token early return inside nested boxing groups (#2775)
* fix alignment-token early return inside nested boxing groups digestNextBody() was prematurely returning on \cr/\lx@hidden@cr tokens even when deep inside nested hbox/vbox groups within a tabular cell. This caused cascading errors when \adjustbox{valign=t}{\begin{subfigure}...} was used inside \tabular, as the \\ inside subfigure leaked out as \lx@hidden@cr and triggered the alignment early-return check. Fix: guard the alignment-token check with a boxing-depth comparison so it only fires at the original alignment nesting level, not inside deeper boxing groups. Also includes minor whitespace reformatting by latexmllint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * add frameDepth guard to readBoxContents scanner; rebind \\ in beforeFloat --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 50f0061 commit 7050832

File tree

3 files changed

+168
-165
lines changed

3 files changed

+168
-165
lines changed

lib/LaTeXML/Core/Stomach.pm

Lines changed: 65 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ sub digestNextBody {
100100
my @aug = ();
101101

102102
while (defined($token = $$self{gullet}->getPendingComment || $$self{gullet}->readXToken(1))) {
103-
if ($alignment && scalar(@LaTeXML::LIST) && (Equals($token, T_ALIGN) ||
103+
if ($alignment && scalar(@LaTeXML::LIST)
104+
&& (scalar(@{ $$self{boxing} }) <= $initdepth) # Only at alignment nesting level
105+
&& (Equals($token, T_ALIGN) ||
104106
Equals($token, T_CS('\cr')) || Equals($token, T_CS('\lx@hidden@cr')) ||
105107
Equals($token, T_CS('\lx@hidden@crcr')))) {
106108
# at least \over calls in here without the intent to passing through the alignment.
@@ -231,13 +233,13 @@ sub invokeToken_simple {
231233
my $cc = $meaning->getCatcode;
232234
my $font = $STATE->lookupValue('font');
233235
if ($cc == CC_SPACE) {
234-
$STATE->clearPrefixes; # prefixes shouldn't apply here.
235-
if($STATE->lookupValue('MODE') =~ /(?:math|vertical)$/) {
236+
$STATE->clearPrefixes; # prefixes shouldn't apply here.
237+
if ($STATE->lookupValue('MODE') =~ /(?:math|vertical)$/) {
236238
return (); }
237239
else {
238240
enterHorizontal($self);
239241
return Box($meaning->toString, $font, $$self{gullet}->getLocator, $meaning); } }
240-
elsif ($cc == CC_COMMENT) { # Note: Comments need char decoding as well!
242+
elsif ($cc == CC_COMMENT) { # Note: Comments need char decoding as well!
241243
my $comment = LaTeXML::Package::FontDecodeString($meaning->toString, undef, 1);
242244
# However, spaces normally would have be digested away as positioning...
243245
my $badspace = pack('U', 0xA0) . "\x{0335}"; # This is at space's pos in OT1
@@ -331,16 +333,16 @@ sub bgroup {
331333

332334
sub egroup {
333335
my ($self) = @_;
334-
if ($STATE->isValueBound('BOUND_MODE', 0)) { # Last stack frame was a mode switch!?!?!
335-
# Don't pop if there's an error; maybe we'll recover?
336+
if ($STATE->isValueBound('BOUND_MODE', 0)) { # Last stack frame was a mode switch!?!?!
337+
# Don't pop if there's an error; maybe we'll recover?
336338
Error('unexpected', $LaTeXML::CURRENT_TOKEN, $self,
337-
"Attempt to close a group that switched to mode ".$STATE->lookupValue('MODE'),
339+
"Attempt to close a group that switched to mode " . $STATE->lookupValue('MODE'),
338340
currentFrameMessage($self)); }
339-
elsif ($STATE->lookupValue('groupNonBoxing')) { # or group was opened with \begingroup
341+
elsif ($STATE->lookupValue('groupNonBoxing')) { # or group was opened with \begingroup
340342
Error('unexpected', $LaTeXML::CURRENT_TOKEN, $self,
341343
"Attempt to close boxing group",
342344
currentFrameMessage($self)); }
343-
else { # Don't pop if there's an error; maybe we'll recover?
345+
else { # Don't pop if there's an error; maybe we'll recover?
344346
popStackFrame($self, 0); }
345347
return; }
346348

@@ -351,16 +353,16 @@ sub begingroup {
351353

352354
sub endgroup {
353355
my ($self) = @_;
354-
if ($STATE->isValueBound('BOUND_MODE', 0)) { # Last stack frame was a mode switch!?!?!
355-
# Don't pop if there's an error; maybe we'll recover?
356+
if ($STATE->isValueBound('BOUND_MODE', 0)) { # Last stack frame was a mode switch!?!?!
357+
# Don't pop if there's an error; maybe we'll recover?
356358
Error('unexpected', $LaTeXML::CURRENT_TOKEN, $self,
357-
"Attempt to close a group that switched to mode ".$STATE->lookupValue('MODE'),
359+
"Attempt to close a group that switched to mode " . $STATE->lookupValue('MODE'),
358360
currentFrameMessage($self)); }
359361
elsif (!$STATE->lookupValue('groupNonBoxing')) { # or group was opened with \bgroup
360362
Error('unexpected', $LaTeXML::CURRENT_TOKEN, $self,
361363
"Attempt to close non-boxing group",
362364
currentFrameMessage($self)); }
363-
else { # Don't pop if there's an error; maybe we'll recover?
365+
else { # Don't pop if there's an error; maybe we'll recover?
364366
popStackFrame($self, 1); }
365367
return; }
366368

@@ -396,77 +398,77 @@ sub endgroup {
396398
#----------------------------------------------------------------------
397399
# These are the only modes that you can beginMode|endMode, and must be entered that way.
398400
our %bindable_mode = (
399-
text => 'restricted_horizontal',
400-
restricted_horizontal => 'restricted_horizontal',
401-
vertical => 'internal_vertical',
402-
internal_vertical => 'internal_vertical',
403-
math => 'math',
404-
inline_math => 'math',
405-
display_math => 'display_math');
401+
text => 'restricted_horizontal',
402+
restricted_horizontal => 'restricted_horizontal',
403+
vertical => 'internal_vertical',
404+
internal_vertical => 'internal_vertical',
405+
math => 'math',
406+
inline_math => 'math',
407+
display_math => 'display_math');
406408

407409
# Switch to horizontal mode, w/o stacking the mode
408410
# Can really only switch to horizontal mode from vertical|internal_vertical,
409411
# so no math, font, etc changes are needed.
410412
sub enterHorizontal {
411-
my($self) = @_;
412-
my $mode = $STATE->lookupValue('MODE');
413-
if($mode =~ /vertical$/){
414-
Debug("MODE enter horizontal, from $mode, for ".Stringify($LaTeXML::CURRENT_TOKEN))
415-
if $LaTeXML::DEBUG{modes};
416-
$STATE->assignValue(MODE => 'horizontal', 'inplace'); } # SAME frame as BOUND_MODE!
417-
elsif (($mode =~ /horizontal$/) || ($mode =~ /math$/)) { } # ignorable?
413+
my ($self) = @_;
414+
my $mode = $STATE->lookupValue('MODE');
415+
if ($mode =~ /vertical$/) {
416+
Debug("MODE enter horizontal, from $mode, for " . Stringify($LaTeXML::CURRENT_TOKEN))
417+
if $LaTeXML::DEBUG{modes};
418+
$STATE->assignValue(MODE => 'horizontal', 'inplace'); } # SAME frame as BOUND_MODE!
419+
elsif (($mode =~ /horizontal$/) || ($mode =~ /math$/)) { } # ignorable?
418420
else {
419-
Warn('unexpected',$mode,$self,
421+
Warn('unexpected', $mode, $self,
420422
"Cannot switch to horizontal mode from $mode"); }
421423
return; }
422424

423425
# Resume vertical mode, if in horizontal mode, by executing \par, in TeX-like fashion.
424426
sub leaveHorizontal {
425-
my($self) = @_;
426-
my $mode = $STATE->lookupValue('MODE');
427-
my $bound = $STATE->lookupValue('BOUND_MODE');
427+
my ($self) = @_;
428+
my $mode = $STATE->lookupValue('MODE');
429+
my $bound = $STATE->lookupValue('BOUND_MODE');
428430
# This needs to be an invisible, and slightly gentler, \par (see \lx@normal@par)
429431
# BUT still allow user defined \par !
430432
if (($mode eq 'horizontal') && ($bound =~ /vertical$/)) {
431433
local $LaTeXML::INTERNAL_PAR = 1;
432-
Debug("MODE leaving $mode via \\par (within $bound), for ".Stringify($LaTeXML::CURRENT_TOKEN))
433-
if $LaTeXML::DEBUG{modes};
434+
Debug("MODE leaving $mode via \\par (within $bound), for " . Stringify($LaTeXML::CURRENT_TOKEN))
435+
if $LaTeXML::DEBUG{modes};
434436
push(@LaTeXML::LIST, $self->invokeToken(T_CS('\par'))); }
435437
return; }
436438

437439
# Repack recently digested horizontal items into single horizontal List.
438440
# Note that TeX would have done paragraph line-breaking, resulting in essentially
439441
# a vertical list.
440442
sub repackHorizontal {
441-
my($self)=@_;
443+
my ($self) = @_;
442444
my @para = ();
443445
my $item;
444446
my $mode;
445447
my $keep = 0;
446-
while(@LaTeXML::LIST
447-
&& ($item = $LaTeXML::LIST[-1])
448-
&& (($mode = ($item->getProperty('mode')||'horizontal'))
449-
=~ /^(?:horizontal|restricted_horizontal|math)$/)) {
448+
while (@LaTeXML::LIST
449+
&& ($item = $LaTeXML::LIST[-1])
450+
&& (($mode = ($item->getProperty('mode') || 'horizontal'))
451+
=~ /^(?:horizontal|restricted_horizontal|math)$/)) {
450452
# if ONLY horizontal mode spaces, we can prune them; it just makes an empty ltx:p
451-
$keep = 1 if ($mode ne 'horizontal') || ! $item->getProperty('isSpace');
452-
unshift(@para,pop(@LaTeXML::LIST)); }
453-
push(@LaTeXML::LIST, List(@para, mode=>'horizontal')) if $keep;
453+
$keep = 1 if ($mode ne 'horizontal') || !$item->getProperty('isSpace');
454+
unshift(@para, pop(@LaTeXML::LIST)); }
455+
push(@LaTeXML::LIST, List(@para, mode => 'horizontal')) if $keep;
454456
return; }
455457

456458
# Resume vertical mode, internal form: reset mode, and repacks recently
457459
# digested horizontal items. This is useful within argument digestion, eg.
458460
sub leaveHorizontal_internal {
459-
my($self) = @_;
460-
my $mode = $STATE->lookupValue('MODE');
461-
my $bound = $STATE->lookupValue('BOUND_MODE');
461+
my ($self) = @_;
462+
my $mode = $STATE->lookupValue('MODE');
463+
my $bound = $STATE->lookupValue('BOUND_MODE');
462464
# This needs to be an invisible, and slightly gentler, \par (see \lx@normal@par)
463465
# BUT still allow user defined \par !
464466
if (($mode eq 'horizontal') && ($bound =~ /vertical$/)) {
465-
Debug("MODE leave $mode, resuming $bound, for ".Stringify($LaTeXML::CURRENT_TOKEN))
466-
if $LaTeXML::DEBUG{modes};
467+
Debug("MODE leave $mode, resuming $bound, for " . Stringify($LaTeXML::CURRENT_TOKEN))
468+
if $LaTeXML::DEBUG{modes};
467469
repackHorizontal($self);
468470
$STATE->assignValue(MODE => $bound, 'inplace'); }
469-
return; }
471+
return; }
470472

471473
# Mode switch to $umode; generally pushes a new stack frame, sets various state variables
472474
# In RARE cases, we need to do this WITHOUT a new stack frome (eg. \begin{document})
@@ -476,16 +478,16 @@ sub beginMode {
476478
if (my $mode = $bindable_mode{$umode}) {
477479
my $prevmode = $STATE->lookupValue('MODE');
478480
my $prevbound = $STATE->lookupValue('BOUND_MODE');
479-
my $ismath = $mode =~ /math$/;
481+
my $ismath = $mode =~ /math$/;
480482
my $wasmath = $prevmode =~ /math$/;
481-
pushStackFrame($self) unless $noframe; # Effectively bgroup
482-
$STATE->assignValue(BOUND_MODE => $mode, 'local'); # New value within this frame!
483+
pushStackFrame($self) unless $noframe; # Effectively bgroup
484+
$STATE->assignValue(BOUND_MODE => $mode, 'local'); # New value within this frame!
483485
$STATE->assignValue(MODE => $mode, 'local');
484486
$STATE->assignValue(IN_MATH => $ismath, 'local');
485487
Debug("MODE bind $mode, from $prevmode "
486-
.($prevbound eq $prevmode ? '' : "(in $prevbound) ")
487-
.", for ".Stringify($LaTeXML::CURRENT_TOKEN))
488-
if $LaTeXML::DEBUG{modes};
488+
. ($prevbound eq $prevmode ? '' : "(in $prevbound) ")
489+
. ", for " . Stringify($LaTeXML::CURRENT_TOKEN))
490+
if $LaTeXML::DEBUG{modes};
489491
my $curfont = $STATE->lookupValue('font');
490492
if ($mode eq $prevbound) { }
491493
elsif ($ismath) {
@@ -505,15 +507,15 @@ sub beginMode {
505507
my $ereg = $STATE->lookupDefinition($every);
506508
if (my $toks = $ereg && $ereg->isRegister && $ereg->valueOf()) {
507509
$self->getGullet->unread($toks); } }
508-
elsif($wasmath) {
510+
elsif ($wasmath) {
509511
# When entering text mode, we should set the font to the text font in use before the math
510512
# but inherit color and size
511513
$STATE->assignValue(font => $STATE->lookupValue('savedfont')->merge(
512514
color => $curfont->getColor, background => $curfont->getBackground,
513515
size => $curfont->getSize), 'local'); }
514516
}
515517
else {
516-
Warn('unexpected',$mode,$self, "Cannot enter $mode mode"); }
518+
Warn('unexpected', $mode, $self, "Cannot enter $mode mode"); }
517519
return; }
518520

519521
# End the mode $umode; generally pops the stack frome.
@@ -524,20 +526,20 @@ sub endMode {
524526
if (my $mode = $bindable_mode{$umode}) {
525527
if ((!$STATE->isValueBound('BOUND_MODE', 0)) # Last stack frame was NOT a mode switch!?!?!
526528
|| ($STATE->lookupValue('BOUND_MODE') ne $mode)) { # Or was a mode switch to a different mode
527-
# Don't pop if there's an error; maybe we'll recover?
529+
# Don't pop if there's an error; maybe we'll recover?
528530
Error('unexpected', $LaTeXML::CURRENT_TOKEN, $self, "Attempt to end mode $mode",
529531
currentFrameMessage($self)); }
530532
else {
531-
leaveHorizontal_internal($self) if $mode =~ /vertical$/; # nopar version!
533+
leaveHorizontal_internal($self) if $mode =~ /vertical$/; # nopar version!
532534
if ($noframe) {
533-
$self->executeBeforeAfterGroup; } # No pop, but at least, do beforeAfterGrup
535+
$self->executeBeforeAfterGroup; } # No pop, but at least, do beforeAfterGrup
534536
else {
535-
popStackFrame($self); } # Effectively egroup.
536-
Debug("MODE unbind $mode, resume ".$STATE->lookupValue('MODE').", for ".Stringify($LaTeXML::CURRENT_TOKEN))
537-
if $LaTeXML::DEBUG{modes};
538-
}}
537+
popStackFrame($self); } # Effectively egroup.
538+
Debug("MODE unbind $mode, resume " . $STATE->lookupValue('MODE') . ", for " . Stringify($LaTeXML::CURRENT_TOKEN))
539+
if $LaTeXML::DEBUG{modes};
540+
} }
539541
else {
540-
Warn('unexpected',$mode,$self, "Cannot end $mode mode"); }
542+
Warn('unexpected', $mode, $self, "Cannot end $mode mode"); }
541543
return; }
542544

543545
#**********************************************************************

0 commit comments

Comments
 (0)