File Coverage

blib/lib/Text/HTML/Turndown/Tables.pm
Criterion Covered Total %
statement 60 66 90.9
branch 7 10 70.0
condition 3 9 33.3
subroutine 12 13 92.3
pod 0 4 0.0
total 82 102 80.3


line stmt bran cond sub pod time code
1             package Text::HTML::Turndown::Tables 0.12;
2 1     1   868 use 5.020;
  1         5  
3 1     1   6 use experimental 'signatures';
  1         2  
  1         11  
4 1     1   231 use stable 'postderef';
  1         2  
  1         7  
5 1     1   105 use List::MoreUtils 'all';
  1         31  
  1         22  
6 1     1   942 use List::Util 'max';
  1         2  
  1         117  
7 1     1   931 use Text::Table;
  1         24783  
  1         2351  
8              
9             =head1 NAME
10              
11             Text::HTML::Turndown::Tables - rules for Markdown Tables
12              
13             =head1 SYNOPSIS
14              
15             use Text::HTML::Turndown;
16             my $turndown = Text::HTML::Turndown->new(%$options);
17             $turndown->use('Text::HTML::Turndown::Tables');
18              
19             my $markdown = $convert->turndown(<<'HTML');
20            
Helloworld!
21             HTML
22             # | Hello | world! |
23             # | ----- | ------ |
24              
25             =cut
26              
27             our %RULES = (
28              
29             tableCell => {
30             filter => ['th', 'td'],
31             replacement => sub( $content, $node, $options, $context ) {
32             return cell($content, $node, undef);
33             },
34             },
35              
36             tableRow => {
37             filter => 'tr',
38             replacement => sub( $content, $node, $options, $context ) {
39             my $borderCells = '';
40             my $alignMap = { left => ':--', right => '--:', center => ':-:' };
41              
42             # Eliminate empty rows
43             if( $content =~ m!\A\|( \|)+\z! ) {
44             return '';
45             }
46              
47             if (isHeadingRow($node)) {
48             #warn "Header content: [$content]";
49             my @ch = $node->childNodes;
50             for my $ch ($node->childNodes) {
51             my $border = '---';
52             my $align = lc(
53             $ch->getAttribute('align') || ''
54             );
55              
56             if ($align) {
57             $border = $alignMap->{$align} || $border;
58             }
59              
60             $borderCells .= cell($border, $ch, undef)
61             }
62             }
63             return "\n" . $content . ($borderCells ? "\n" . $borderCells : '')
64             }
65             },
66              
67             table => {
68             filter => ['table'],
69              
70             replacement => sub( $content, $node, $options, $context ) {
71             # Ensure there are no blank lines
72             $content =~ s/\n\n/\n/g;
73             $content =~ s/^\s*//;
74             # Re-parse and re-layout the table:
75             my @table = split /\r?\n/, $content;
76             my @new_table;
77             my @column_width;
78             for my $row (@table) {
79             $row =~ s!^\|\s*!!;
80             $row =~ s!\s+\|\s*\z!!;
81             my @cols = map {s!^\s+!!; s!\s+\z!!r; } split /\|/, $row;
82             push @new_table, \@cols;
83             };
84             my $h = shift @new_table;
85             $h = [map { $_, \" | " } $h->@*];
86             pop $h->@*;
87             unshift $h->@*, \"| ";
88             push $h->@*, \" |";
89             my $table = Text::Table->new(
90             $h->@*,
91             );
92             #shift @new_table;
93             $table->load( @new_table );
94             $content = "\n\n" . $table . "\n\n";
95             },
96              
97             },
98              
99             tableSection => {
100             filter => ['thead', 'tbody', 'tfoot'],
101             replacement => sub( $content, $node, $options, $context ) {
102             return $content
103             }
104             }
105             );
106              
107             # A tr is a heading row if:
108             # - the parent is a THEAD
109             # - or if its the first child of the TABLE or the first TBODY (possibly
110             # following a blank THEAD)
111             # - and every cell is a TH
112 35     35 0 67 sub isHeadingRow ($tr) {
  35         55  
  35         68  
113 35 50       99 return if ! $tr;
114 35         776 my $parentNode = $tr->parentNode;
115 35 50       1262 my $n = $tr->can('_node') ? $tr->_node : $tr;
116             return (
117             uc ($parentNode->nodeName) eq 'THEAD' ||
118             (
119             $n->isEqual($parentNode->firstChild)
120             && (uc $parentNode->nodeName eq 'TABLE' || isFirstTbody($parentNode))
121 35   66 7   648 && all { uc($_->nodeName) eq 'TH' } $tr->childNodes
  7         436  
122             )
123             )
124             }
125              
126 11     11 0 48 sub isFirstTbody ($element) {
  11         21  
  11         19  
127 11         38 my $previousSibling = $element->previousSibling;
128             return (
129 11   33     138 uc $element->nodeName eq 'TBODY'
130             && (
131             !$previousSibling ||
132             (
133             uc $previousSibling->nodeName eq 'THEAD'
134             && $previousSibling->textContent =~ /^\s*$/
135             )
136             )
137             )
138             }
139              
140 101     101 0 161 sub cell ($content, $node, $escape=1) {
  101         156  
  101         147  
  101         167  
  101         142  
141 101         1905 my $first = !$node->previousSibling;
142 101         2841 my $prefix = ' ';
143 101 100       916 if ($first) { $prefix = '| ' };
  48         91  
144              
145             # We assume that we have no further HTML tags contained in $content
146             # convert all elements in $content into their Markdown equivalents
147 101 50       226 if( $escape ) {
148 0         0 $content = Text::HTML::Turndown->escape( $content );
149             }
150              
151             # Fix up newlines
152 101         239 $content =~ s!\r?\n!
!g;
153              
154 101         523 return $prefix . $content . ' |'
155             }
156              
157 22     22 0 56 sub install ($class, $target) {
  22         47  
  22         47  
  22         42  
158 22     22   41 $target->preprocess(sub($tree) {
  22         51  
  22         44  
159             # We will likely need other/more rules to turn arbitrary HTML
160             # into what a browser has as DOM for tables
161 22         152 for my $table ($tree->find('//table')->@*) {
162             # Turn
...
163             # into
...
164 13 100       579 if( $table->find( './thead/td' )->@* ) {
165 1         34 my $head = $table->find('./thead',$table)->shift;
166 1         43 my $tr = $head->ownerDocument->createElement('tr');
167 1         7 $tr->appendChild($_) for $head->childNodes;
168 1         59 $head->appendChild( $tr );
169             }
170             }
171 22         794 return $tree;
172 22         220 });
173 0     0     $target->keep(sub ($node) {
  0            
  0            
174 0         0 my $firstRow = $node->find('.//tr')->shift;
175 0   0     0 return uc $node->nodeName eq 'TABLE' && !isHeadingRow($firstRow)
176 22         148 });
177 22         105 for my $key (keys %RULES) {
178 88         268 $target->addRule($key, $RULES{$key})
179             }
180             }
181              
182             1;
183              
184             =head1 REPOSITORY
185              
186             The public repository of this module is
187             L.
188              
189             =head1 SUPPORT
190              
191             The public support forum of this module is L.
192              
193             =head1 BUG TRACKER
194              
195             Please report bugs in this module via the Github bug queue at
196             L
197              
198             =head1 AUTHOR
199              
200             Max Maischein C
201              
202             =head1 COPYRIGHT (c)
203              
204             Copyright 2025- by Max Maischein C.
205              
206             =head1 LICENSE
207              
208             This module is released under the Artistic License 2.0.
209              
210             =cut