| 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
|
|
|
|
|
|
|
|
|
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 |
|
|