Skip to content

Commit

Permalink
More empty/spacing fixes (#2262)
Browse files Browse the repository at this point in the history
* Rename \@@eat@space to \lx@column@trimright to avoid confusion (it's NOT a LaTeX name), and to only trim explicit spaces, not spacing commands

* Let space take up space when sizing alignment cells

* IsEmpty should NOT count spaces as empty; define new IsEmptyOrSpace in case that's actually needed

* Avoid some confusions about space vs empty

* Update changed tests

* Removed tentative general IsEmptyOrSpace since any practical usecase has peculiar, specific needs

* Whatsit property the_box (typically one of the arguments) stands in for the content when checking emptiness and related tests

* Define \lx@intercol as analog for LaTeX's \@acol; manage and record spacing added around alignment columns

* Distinguish empty vs spacing alignment cells; Careful about pruning Alignment rows/columns that contain spaces if bordered; prepare for CSS padding to deal with spacing

* Update test cases for careful alignment pruning

* Add to tabular test case for rows & columns that should not be pruned

* Dont prematurely strip empty boxes from alignment; repair regression in test cases

* Recognize alignment cell padding (\lx@intercol) and record

* Update test cases for better handling of cell padding

* Disabling intercolumn spacing is more a general Template thing, than a specific column

* Record all alignment cell leading/trailing space as potential padding; use to determine css; simplify \lx@intercol and its usage

* Update CSS for alignment cell padding control

* Update tabbing for alignment padding

* Update test cases for better padding control

* Make grouping commands { and } be alignment skippable so we detect spacing within groups

* Don't prematurely increment cell with by l/rpadding; do account for vertical size of l/rpadding

* Fix typo

* Fix tests again for latest tweaks

* Rename the_body to content_box
  • Loading branch information
brucemiller authored Dec 14, 2023
1 parent faa73d9 commit ec9cb83
Show file tree
Hide file tree
Showing 21 changed files with 694 additions and 468 deletions.
152 changes: 124 additions & 28 deletions lib/LaTeXML/Core/Alignment.pm
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,6 @@ sub getColumnAfter {
my ($self) = @_;
my $column;
if (($column = $self->currentColumn) && !$$column{omitted}) {
# Possible \@@eat@space ??? (if LaTeX style???)
return Tokens(@{ $$column{after} }, T_CS('\@column@after')); }
else {
return Tokens(); } }
Expand Down Expand Up @@ -329,7 +328,23 @@ sub beAbsorbed {
# Normalize the border attribute
my $border = join(' ', sort(map { split(/ */, $_) } $$cell{border} || ''));
$border =~ s/(.) \1/$1$1/g;
my $empty = $$cell{empty} || !$$cell{boxes} || !scalar($$cell{boxes}->unlist);
my @classes = ($$cell{class});
my $empty = $$cell{empty};
my ($pre, $post);
# Heuristic: use common td CSS padding, or none, or explicit spacing within cell
# Ideally, we'd compare to a current \(tab|array)colsep
# and create a special CSS class for non-standard values
my $lpad = ($$cell{lspaces} ? $$cell{lspaces}->getWidth->valueOf : 0);
my $rpad = ($$cell{rspaces} ? $$cell{rspaces}->getWidth->valueOf : 0);
if ((!$empty || $$cell{boxes}) && ($lpad < Dimension('0.2em')->valueOf)) {
push(@classes, 'ltx_nopad_l') unless $ismath; }
elsif ($lpad < Dimension('1.5em')->valueOf) { } # do nothing
else { $pre = $$cell{lspaces}; }
if ((!$empty || $$cell{boxes}) && ($rpad < Dimension('0.2em')->valueOf)) {
push(@classes, 'ltx_nopad_r') unless $ismath; }
elsif ($rpad < Dimension('1.5em')->valueOf) { } # do nothing
else { $post = $$cell{rspaces}; }
my $class = join(' ', grep { $_; } @classes);
$$cell{cell} = &{ $$self{openColumn} }($document,
align => $$cell{align}, width => $$cell{width},
vattach => $$cell{vattach},
Expand All @@ -341,11 +356,14 @@ sub beAbsorbed {
# Which properties do we expose to the constructor?
x => $$cell{x}, y => $$cell{y},
cwidth => $$cell{cwidth}, cheight => $$cell{cheight}, cdepth => $$cell{cdepth},
class => $class
);
if (!$empty) {
if (!$$cell{skippable}) {
local $LaTeXML::BOX = $$cell{boxes};
$document->openElement('ltx:XMArg', rule => 'Anything,') if $ismath; # Hacky!
$document->absorb($pre) if $pre;
$document->absorb($$cell{boxes});
$document->absorb($post) if $post;
$document->closeElement('ltx:XMArg') if $ismath;
}
&{ $$self{closeColumn} }($document); }
Expand Down Expand Up @@ -406,33 +424,88 @@ sub normalize_cell_sizes {
my ($self) = @_;
# Examines: boxes, align, vattach
# Sets: cwidth, cheight, cdepth (per cell) & empty
my $i = -1;
foreach my $row (@{ $$self{rows} }) {
my $j = -1;
$i++;
# Do we need to account for any space in the $$row{before} or $$row{after}?
foreach my $cell (@{ $$row{columns} }) {
$j++;
if (my $boxes = $$cell{boxes}) {
my ($w, $h, $d, $cw, $ch, $cd)
= $boxes->getSize(align => $$cell{align}, width => $$cell{width},
vattach => $$cell{vattach});
Debug("CELL (" . join(',', map { $_ . "=" . ToString($$cell{$_}); } qw(align width vattach))
. ") size " . showSize($w, $h, $d)
. " csize " . showSize($cw, $ch, $cd)
. " Boxes=" . ToString($boxes)) if $LaTeXML::DEBUG{halign} && $LaTeXML::DEBUG{size};
my $fullw = $cw;
my ($lpad, $rpad, $padh, $padd);
if (my $lspaces = $$cell{lspaces}) {
($lpad, $padh, $padd) = $lspaces->getSize;
$fullw = ($fullw ? $fullw->add($lpad) : $lpad) if $lpad;
$h = ($h ? $h->larger($padh) : $padh) if $padh;
$d = ($d ? $d->larger($padd) : $padd) if $padd; }
if (my $rspaces = $$cell{rspaces}) {
($rpad, $padh, $padd) = $rspaces->getSize;
$fullw = ($fullw ? $fullw->add($rpad) : $rpad) if $rpad;
$h = ($h ? $h->larger($padh) : $padh) if $padh;
$d = ($d ? $d->larger($padd) : $padd) if $padd; }
my @boxes = $boxes->unlist;
my $isrule = scalar(@boxes)
&& !(grep { !($_->getProperty('isHorizontalRule') || $_->getProperty('isVerticalRule')
|| $_->getProperty('alignmentSkippable')
|| (ref $_ eq 'LaTeXML::Core::Comment')
); } @boxes);
my $empty =
(((!$cw) || $cw->valueOf < 1)
(((!$fullw) || $fullw->valueOf < 1)
&& (((!$ch) || $ch->valueOf < 1)
&& ((!$cd) || $cd->valueOf < 1))
|| !(grep { !($_->getProperty('isSpace') || $_->getProperty('isHorizontalRule') || $_->getProperty('isVerticalRule')); } $boxes->unlist)
|| $isrule
) && !preservedBoxes($boxes);
$$cell{cwidth} = $w || Dimension(0);
$$cell{cheight} = $h || Dimension(0);
$$cell{cdepth} = $d || Dimension(0);
$$cell{empty} = $empty;
$$cell{align} = undef if $empty; }
$$cell{cwidth} = $w || Dimension(0);
$$cell{cheight} = $h || Dimension(0);
$$cell{cdepth} = $d || Dimension(0);
$$cell{lpadding} = $lpad;
$$cell{rpadding} = $rpad;
$$cell{empty} = $empty;
$$cell{skippable} = $empty || isSkippable($boxes);
$$cell{align} = undef if $$cell{skippable};
Debug("CELL[$i,$j] size=" . showSize($w, $h, $d) . " csize " . showSize($cw, $ch, $cd)
. ";\n " . join(',', map { $_ . "=" . ToString($$cell{$_}); } sort keys %$cell)
. "\n Boxes=" . Stringify($boxes)
) if $LaTeXML::DEBUG{alignment_normalize};
}
else {
$$cell{empty} = 1; }
$$cell{empty} = 1;
$$cell{skippable} = 1; }
} }
return; }

# Check whether all these things are "empty" or only spaces or otherwise skippable in a table cell
# Very similar to IsEmpty, but also recognizes spaces or alignmentSkippable items
sub isSkippable {
my (@things) = @_;
foreach my $thing (@things) {
my $ref = ref $thing;
if (!$thing) { }
elsif ($ref eq 'LaTeXML::Core::Comment') { }
elsif ($ref eq 'LaTeXML::Core::Tokens') {
return 0 unless isSkippable($thing->unlist); }
elsif ($ref eq 'LaTeXML::Core::Token') {
my $cc = $$thing[1];
return 0 if ($cc == CC_LETTER) || ($cc == CC_OTHER) || ($cc == CC_ACTIVE) || ($cc == CC_CS); }
elsif ((!$thing->getProperty('isEmpty'))
&& (!$thing->getProperty('isSpace'))
&& (!$thing->getProperty('alignmentSkippable'))) {
if ($ref eq 'LaTeXML::Core::Box') {
my $s = $thing->getString;
return 0 if (defined $s) && ($s !~ /^\s*$/); }
elsif ($ref eq 'LaTeXML::Core::List') {
return 0 unless isSkippable($thing->unlist); }
elsif ($ref eq 'LaTeXML::Core::Whatsit') {
if (my $body = $thing->getProperty('content_box')) {
return 0 unless isSkippable($body); }
else {
return 0; } } } }
return 1; }

sub showSize {
my ($w, $h, $d) = @_;
return '[' . ToString($w) . ' x ' . ToString($h) . ' + ' . ToString($d) . ']'; }
Expand Down Expand Up @@ -535,7 +608,7 @@ sub normalize_sum_sizes {
elsif ($a eq 'right') { $colx = $colx->add($dx); } }
$$cell{x} = $colx;
$$cell{y} = $rowpos[$i];
Debug("CELL[$j,$i] " . showSize($$cell{cwidth}, $$cell{cheight}, $$cell{cdepth})
Debug("CELL[$i,$j] " . showSize($$cell{cwidth}, $$cell{cheight}, $$cell{cdepth})
. " @ " . ToString($$cell{x}) . "," . ToString($$cell{y})
. " w/ " . join(',', map { $_ . '=' . ToString($$cell{$_}); }
(qw(align vattach skipped colspan rowspan))))
Expand Down Expand Up @@ -580,7 +653,7 @@ sub normalize_mark_spans {
if ($rrow) {
for (my $jj = $j ; $jj < $j + $ncspan ; $jj++) {
if (my $ccol = $$rrow{columns}[$jj]) {
if (!$$ccol{empty}) {
if (!$$ccol{skippable}) {
$rowempty = 0; } } } }
if (!$nrc) { }
elsif (!$rrow || !$rowempty) {
Expand Down Expand Up @@ -618,12 +691,26 @@ sub normalize_prune_rows {
my @rows = @{ $$self{rows} };
my @filtered = ();
for (my $i = 0 ; $i < scalar(@rows) ; $i++) {
my $row = $rows[$i];
if (grep { !$$_{empty} } @{ $$row{columns} }) { # Not empty! so keep it
my $row = $rows[$i];
my $next = $rows[$i + 1];
# prunable if all cells are empty
# OR are only spacing NOT made visible by requiring both top & bottom borders
my $prunable = 1;
my $check_bracketting;
foreach my $c (@{ $$row{columns} }) { # Check if all cells are either empty or space only
$check_bracketting = 1 if $$c{skippable} && !$$c{empty};
$prunable = 0 unless $$c{skippable}; }
if ($prunable && $check_bracketting # If spaces, make sure not bracketted by borders
&& scalar(grep { ($$_{border} || '') =~ /t/i } @{ $$row{columns} })) {
if ($next) {
$prunable = 0 if scalar(grep { ($$_{border} || '') =~ /t/i } @{ $$next{columns} }); }
else {
$prunable = 0 if scalar(grep { ($$_{border} || '') =~ /b/i } @{ $$row{columns} }); } }
if (!$prunable) {
push(@filtered, $row); }
elsif (my $next = $rows[$i + 1]) { # Remove empty row, but copy top border to NEXT row
elsif ($next) { # Remove empty row, but copy top border to NEXT row
if ($preserve) {
push(@filtered, $row); next; } # don't remove inner rows from math EXCEPT last row!!
push(@filtered, $row); next; } # don't remove inner rows from math EXCEPT last row!!
my $nc = scalar(@{ $$row{columns} });
my ($pruneh, $pruned) = (0, 0);
for (my $j = 0 ; $j < $nc ; $j++) {
Expand All @@ -638,7 +725,9 @@ sub normalize_prune_rows {
$border =~ s/B/T/g; # but convert to top
$$next{columns}[$j]{border} .= $border; } # add to NEXT row
# This tpadding should be combined w/any extra rowspacing from \\[dim] !
$$next{tpadding} = Dimension($pruneh + $pruned) if $pruneh + $pruned; } # And save padding.
$$next{tpadding} = Dimension($pruneh + $pruned) if $pruneh + $pruned; # And save padding.
Debug("PRUNE ROW $i") if $LaTeXML::DEBUG{alignment_normalize};
}
else { # Remove empty last row, but copy top border to bottom of prev.
my $prev = $filtered[-1];
my $nc = scalar(@{ $$row{columns} });
Expand All @@ -657,7 +746,8 @@ sub normalize_prune_rows {
if (defined $$ccol{rowspanned}) { # skip to spanning column if rowspanned!
$ccol = $rows[$$ccol{rowspanned}]{columns}[$j]; }
$$ccol{border} .= $border; } # add to PREVIOUS row.
$$prev{bpadding} = Dimension($pruneh + $pruned) if $pruneh + $pruned; } # And save padding.
$$prev{bpadding} = Dimension($pruneh + $pruned) if $pruneh + $pruned; # And save padding.
Debug("PRUNE ROW (last) $i") if $LaTeXML::DEBUG{alignment_normalize}; }
}
@rows = @filtered;
$$self{rows} = [@filtered];
Expand Down Expand Up @@ -686,10 +776,13 @@ sub normalize_prune_columns {
my $prev = $$row{columns}[$j - 1];
if (my $jj = $$prev{colspanned}) {
$prev = $$row{columns}[$jj]; }
$border =~ s/[^rRlL]//g; # mask all but left border
$border =~ s/l/r/g; # convert to right
$border =~ s/L/R/g; # convert to right
$border =~ s/[^rRlL]//g; # mask all but left border
$border =~ s/l/r/g; # convert to right
$border =~ s/L/R/g; # convert to right
$$prev{border} .= $border;
# Copy left spacing to right column, as well
$$prev{rspaces} = LaTeXML::Core::List::List($$prev{rspaces} || (), $$col{lspaces} || ()) if $$col{lspaces};
$$prev{rpadding} = $$prev{rspaces}->getWidth if $$prev{rspaces};
if (my @preserve = preservedBoxes($$col{boxes})) { # Copy boxes over, in case side effects?
$$prev{boxes} = LaTeXML::Core::List($$prev{boxes}
? ($$prev{boxes}->unlist, @preserve) : @preserve); }
Expand All @@ -705,14 +798,16 @@ sub normalize_prune_columns {
} # Now, remove the column
$$row{columns} = [grep { $_ ne $col } @{ $$row{columns} }];
} }
$prunew = Dimension($prunew);
if ($j) { # If not 1st row, add right padding to previous column
foreach my $row (@rows) {
if (my $col = $$row{columns}[$j - 1]) {
$$col{rpadding} = Dimension($prunew); } } }
$$col{rpadding} = ($$col{rpadding} ? $$col{rpadding}->add($prunew) : $prunew); } } }
else { # Else add left padding to (newly) first column
foreach my $row (@rows) { # And add the padding to previous column
if (my $col = $$row{columns}[0]) {
$$col{lpadding} = Dimension($prunew); } } }
$$col{lpadding} = ($$col{lpadding} ? $$col{lpadding}->add($prunew) : $prunew); } } }
Debug("PRUNE COLUMN $j") if $LaTeXML::DEBUG{alignment_normalize};
} } }
return; }

Expand Down Expand Up @@ -769,6 +864,7 @@ sub ReadAlignmentTemplate {
last unless $nopens; }
push(@tokens, T_END);
$LaTeXML::BUILD_TEMPLATE->setReversion(@tokens);
$LaTeXML::BUILD_TEMPLATE->finish;
return $LaTeXML::BUILD_TEMPLATE; }

sub parseAlignmentTemplate {
Expand Down
38 changes: 29 additions & 9 deletions lib/LaTeXML/Core/Alignment/Template.pm
Original file line number Diff line number Diff line change
Expand Up @@ -53,37 +53,50 @@ sub addBeforeColumn {
unshift(@{ $$self{save_before} }, @tokens); # NOTE: goes all the way to front!
return; }

# NOTE: \@@eat@space should ONLY be added to LaTeX tabular style templates!!!!
# NOTE: \lx@column@trimright should ONLY be added to LaTeX tabular style templates!!!!
# NOT \halign style templates!
sub addAfterColumn {
my ($self, @tokens) = @_;
$$self{current_column}{after} = Tokens(T_CS('\@@eat@space'),
$$self{current_column}{after} = Tokens(T_CS('\lx@column@trimright'),
@tokens, @{ $$self{current_column}{after} });
return; }

# Or between this column & next...
sub addBetweenColumn {
my ($self, @tokens) = @_;
my @cols = @{ $$self{columns} };
if ($$self{current_column}) {
if (my $col = $$self{current_column}) {
if (!$$self{disabled_intercolumn}) {
unshift(@tokens, T_CS('\lx@intercol')); }
$$self{current_column}{after} = Tokens(@{ $$self{current_column}{after} }, @tokens); }
else {
push(@{ $$self{save_between} }, @tokens); }
return; }

sub disableIntercolumn {
my ($self) = @_;
if (my $col = $$self{current_column}) {
$$self{disabled_intercolumn} = 1; }
return; }

sub addColumn {
my ($self, %properties) = @_;
my $col = {%properties};
my $col = {%properties};
if (my $prev = $$self{current_column}) {
$$prev{after} = Tokens(($$prev{after} || ()), T_CS('\lx@intercol'))
unless $$self{disabled_intercolumn}; }
my @before = ();
push(@before, @{ $$self{save_between} }) if $$self{save_between};
push(@before, @{ $$self{save_between} }) if $$self{save_between};
push(@before, T_CS('\lx@intercol')) unless $$self{disabled_intercolumn};
delete $$self{disabled_intercolumn};

push(@before, $properties{before}->unlist) if $properties{before};
push(@before, @{ $$self{save_before} }) if $$self{save_before};
$$col{before} = Tokens(@before);
my @after = ();
push(@after, T_CS('\@@eat@space'));
push(@after, T_CS('\lx@column@trimright'));
push(@after, $properties{after}->unlist) if $properties{after};
$$col{after} = Tokens(@after);
### $$col{after} = Tokens() unless $properties{after};
$$col{after} = Tokens(@after);
$$col{thead} = $properties{thead};
$$col{empty} = 1;
$$self{save_between} = [];
Expand All @@ -97,14 +110,21 @@ sub addColumn {
push(@{ $$self{columns} }, $col); }
return; }

sub finish {
my ($self) = @_;
if (my $prev = $$self{current_column}) {
$$prev{after} = Tokens(($$prev{after} || ()), T_CS('\lx@intercol'))
unless $$self{disabled_intercolumn}; }
return; }

# Methods for using a template.
sub clone {
my ($self) = @_;
my @dup = ();
foreach my $cell (@{ $$self{columns} }) {
push(@dup, {%$cell}); }
return bless { columns => [@dup],
repeated => $$self{repeated}, non_repeating => $$self{non_repeating},
repeated => $$self{repeated}, non_repeating => $$self{non_repeating},
repeating => $$self{repeating} }, ref $self; }

sub show {
Expand Down
15 changes: 8 additions & 7 deletions lib/LaTeXML/Package.pm
Original file line number Diff line number Diff line change
Expand Up @@ -974,9 +974,8 @@ sub TokenizeInternal {
local $STATE = $STY_CATTABLE;
return LaTeXML::Core::Mouth->new($string)->readTokens; }

# Check whether all these things are "empty" (spaces are empty!!)
# Check whether all these things are "empty" (spaces are NOT empty!!)
# short-circuit: return 0 quickly if anything is NOT empty.
# Will we need to distinguish between "empty" and "not visible"??
sub IsEmpty {
my (@things) = @_;
foreach my $thing (@things) {
Expand All @@ -988,16 +987,18 @@ sub IsEmpty {
elsif ($ref eq 'LaTeXML::Core::Token') {
my $cc = $$thing[1];
return 0 if ($cc == CC_LETTER) || ($cc == CC_OTHER) || ($cc == CC_ACTIVE) || ($cc == CC_CS); }
elsif ((!$thing->getProperty('isEmpty'))
&& (!$thing->getProperty('isSpace'))) { # A space-like thing
elsif (!$thing->getProperty('isEmpty')) {
if ($ref eq 'LaTeXML::Core::Box') {
my $s = $thing->getString;
return 0 if (defined $s) && ($s !~ /^\s*$/); }
return 0 if (defined $s) && length($s); }
elsif ($ref eq 'LaTeXML::Core::List') {
return 0 unless IsEmpty($thing->unlist); }
elsif ($ref eq 'LaTeXML::Core::Whatsit') {
return 0 unless ($thing->getDefinition eq $STATE->lookupDefinition(T_BEGIN))
&& IsEmpty($thing->getBody->unlist); } } }
# Sneaky Whatsit property for something (an arg) that stands-in for the whatsit's content.
if (my $body = $thing->getProperty('content_box')) {
return 0 unless IsEmpty($body); }
else {
return 0; } } } }
return 1; }

#======================================================================
Expand Down
Loading

0 comments on commit ec9cb83

Please sign in to comment.