/[suikacvs]/markup/html/whatpm/Whatpm/HTMLTable.pm
Suika

Contents of /markup/html/whatpm/Whatpm/HTMLTable.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.11 - (hide annotations) (download)
Tue May 6 07:49:16 2008 UTC (18 years, 2 months ago) by wakaba
Branch: MAIN
Changes since 1.10: +188 -3 lines
++ whatpm/Whatpm/ChangeLog	6 May 2008 07:48:52 -0000
2008-05-06  Wakaba  <wakaba@suika.fam.cx>

	* HTMLTable.pm (assign_header): New function; first version
	with no support for headers="".
	(form_table): Include table width and height to the returned
	table object for covenience.  Indexing in column assignement was wrong.
	Set whether a data cell is empty or not for convenience.

1 wakaba 1.1 package Whatpm::HTMLTable;
2     use strict;
3    
4     ## An implementation of "Forming a table" algorithm in HTML5
5 wakaba 1.9 sub form_table ($$$;$) {
6     my (undef, $table_el, $onerror, $must_level) = @_;
7 wakaba 1.2 $onerror ||= sub { };
8 wakaba 1.9 $must_level ||= 'm';
9 wakaba 1.1
10     ## Step 1
11 wakaba 1.7 my $x_width = 0;
12 wakaba 1.1
13     ## Step 2
14 wakaba 1.7 my $y_height = 0;
15 wakaba 1.1 my $y_max_node;
16 wakaba 1.8
17     ## Step 3
18     my $pending_tfoot = [];
19 wakaba 1.1
20 wakaba 1.8 ## Step 4
21 wakaba 1.1 my $table = {
22     #caption
23     column => [],
24     column_group => [],
25 wakaba 1.9 row => [], ## NOTE: HTML5 algorithm doesn't associate rows with <tr>s.
26 wakaba 1.1 row_group => [],
27     cell => [],
28 wakaba 1.11 height => 0,
29     width => 0,
30 wakaba 1.1 };
31    
32 wakaba 1.9 my @column_has_anchored_cell;
33     my @row_has_anchored_cell;
34 wakaba 1.1 my @column_generated_by;
35 wakaba 1.9 my @row_generated_by;
36    
37     ## Step 5
38     my @table_child = @{$table_el->child_nodes};
39     return $table unless @table_child;
40    
41 wakaba 1.10 ## Step 6
42     for (0..$#table_child) {
43     my $el = $table_child[$_];
44     next unless $el->node_type == 1; # ELEMENT_NODE
45     next unless $el->manakai_local_name eq 'caption';
46     my $nsuri = $el->namespace_uri;
47     next unless defined $nsuri;
48     next unless $nsuri eq q<http://www.w3.org/1999/xhtml>;
49     $table->{caption} = {element => $el};
50     splice @table_child, $_, 1, ();
51     last;
52     }
53    
54 wakaba 1.9 my $process_row_group;
55     my $end = sub {
56 wakaba 1.10 ## Step 19 (End)
57 wakaba 1.9 for (@$pending_tfoot) {
58     $process_row_group->($_);
59     }
60    
61 wakaba 1.10 ## Step 20
62 wakaba 1.7 for (0 .. $x_width - 1) {
63 wakaba 1.9 unless ($column_has_anchored_cell[$_]) {
64 wakaba 1.1 if ($table->{column}->[$_]) {
65     $onerror->(type => 'column with no anchored cell',
66 wakaba 1.9 node => $table->{column}->[$_]->{element},
67     level => $must_level);
68 wakaba 1.1 } else {
69 wakaba 1.4 $onerror->(type => 'colspan creates column with no anchored cell',
70 wakaba 1.9 node => $column_generated_by[$_],
71     level => $must_level);
72 wakaba 1.1 }
73 wakaba 1.9 last; # only one error.
74 wakaba 1.1 }
75     }
76 wakaba 1.9 for (0 .. $y_height - 1) {
77     unless ($row_has_anchored_cell[$_]) {
78     if ($table->{row}->[$_]) {
79     $onerror->(type => 'row with no anchored cell',
80     node => $table->{row}->[$_]->{element},
81     level => $must_level);
82     } else {
83     $onerror->(type => 'rowspan creates row with no anchored cell',
84     node => $row_generated_by[$_],
85     level => $must_level);
86     }
87     last; # only one error.
88     }
89     }
90    
91 wakaba 1.10 ## Step 21
92 wakaba 1.9 #return $table;
93     }; # $end
94 wakaba 1.1
95 wakaba 1.10 ## Step 7, 8
96 wakaba 1.1 my $current_element;
97     my $current_ln;
98     NEXT_CHILD: {
99     $current_element = shift @table_child;
100     if (defined $current_element) {
101     redo NEXT_CHILD unless $current_element->node_type == 1;
102     my $nsuri = $current_element->namespace_uri;
103     redo NEXT_CHILD unless defined $nsuri and
104     $nsuri eq q<http://www.w3.org/1999/xhtml>;
105     $current_ln = $current_element->manakai_local_name;
106    
107     redo NEXT_CHILD unless {
108     colgroup => 1,
109     thead => 1,
110     tbody => 1,
111     tfoot => 1,
112     tr => 1,
113     }->{$current_ln};
114     } else {
115 wakaba 1.8 ## Step 6 2nd paragraph
116 wakaba 1.9 $end->();
117 wakaba 1.11 $table->{width} = $x_width;
118     $table->{height} = $y_height;
119 wakaba 1.9 return $table;
120 wakaba 1.1 }
121     } # NEXT_CHILD
122    
123 wakaba 1.10 ## Step 9
124     while ($current_ln eq 'colgroup') { # Step 9, Step 9.4
125     ## Step 9.1: column groups
126 wakaba 1.1 my @col = grep {
127     $_->node_type == 1 and
128     defined $_->namespace_uri and
129     $_->namespace_uri eq q<http://www.w3.org/1999/xhtml> and
130     $_->manakai_local_name eq 'col'
131     } @{$current_element->child_nodes};
132     if (@col) {
133     ## Step 1
134 wakaba 1.7 my $x_start = $x_width;
135 wakaba 1.1
136     ## Step 2, 6
137     while (@col) {
138     my $current_column = shift @col;
139    
140     ## Step 3: columns
141     my $span = 1;
142     my $col_span = $current_column->get_attribute_ns (undef, 'span');
143     ## Parse non-negative integer
144     if (defined $col_span and $col_span =~ /^[\x09-\x0D\x20]*([0-9]+)/) {
145     $span = $1 || 1;
146     }
147     ## ISSUE: If span=0, what is /span/ value?
148    
149     ## Step 4, 5
150 wakaba 1.11 $table->{column}->[$x_width++] = {element => $current_column}
151 wakaba 1.7 for 1..$span;
152 wakaba 1.1 }
153    
154     ## Step 7
155     my $cg = {element => $current_element,
156 wakaba 1.7 x => $x_start, y => 0,
157     width => $x_width - $x_start};
158     $table->{column_group}->[$_] = $cg for $x_start .. $x_width - 1;
159 wakaba 1.1 } else { # no <col> children
160     ## Step 1
161     my $span = 1;
162     my $col_span = $current_element->get_attribute_ns (undef, 'span');
163     ## Parse non-negative integer
164     if (defined $col_span and $col_span =~ /^[\x09-\x0D\x20]*([0-9]+)/) {
165     $span = $1 || 1;
166     }
167     ## ISSUE: If span=0, what is /span/ value?
168    
169     ## Step 2
170 wakaba 1.7 $x_width += $span;
171 wakaba 1.1
172     ## Step 3
173     my $cg = {element => $current_element,
174 wakaba 1.7 x => $x_width - $span, y => 0,
175 wakaba 1.1 width => $span};
176 wakaba 1.7 $table->{column_group}->[$_] = $cg for $cg->{x} .. $x_width - 1;
177 wakaba 1.1 }
178    
179 wakaba 1.10 ## Step 9.2, 9.3
180 wakaba 1.1 NEXT_CHILD: {
181     $current_element = shift @table_child;
182     if (defined $current_element) {
183     redo NEXT_CHILD unless $current_element->node_type == 1;
184     my $nsuri = $current_element->namespace_uri;
185     redo NEXT_CHILD unless defined $nsuri and
186     $nsuri eq q<http://www.w3.org/1999/xhtml>;
187     $current_ln = $current_element->manakai_local_name;
188    
189     redo NEXT_CHILD unless {
190     colgroup => 1,
191     thead => 1,
192     tbody => 1,
193     tfoot => 1,
194     tr => 1,
195     }->{$current_ln};
196     } else {
197     ## End of subsection
198 wakaba 1.9
199 wakaba 1.1 ## Step 5 of overall steps 2nd paragraph
200 wakaba 1.9 $end->();
201 wakaba 1.11 $table->{width} = $x_width;
202     $table->{height} = $y_height;
203 wakaba 1.9 return $table;
204 wakaba 1.1 }
205     } # NEXT_CHILD
206     }
207    
208 wakaba 1.10 ## Step 10
209 wakaba 1.1 my $y_current = 0;
210    
211 wakaba 1.10 ## Step 11
212 wakaba 1.1 my @downward_growing_cells;
213    
214     my $growing_downward_growing_cells = sub {
215     for (@downward_growing_cells) {
216     for my $x ($_->[1] .. ($_->[1] + $_->[2] - 1)) {
217     $table->{cell}->[$x]->[$y_current] = [$_->[0]];
218     $_->[0]->{height}++;
219     }
220     }
221     }; # $growing_downward_growing_cells
222    
223     my $process_row = sub {
224     ## Step 1
225 wakaba 1.7 $y_height++ if $y_height == $y_current;
226 wakaba 1.1
227     ## Step 2
228 wakaba 1.7 my $x_current = 0;
229    
230 wakaba 1.1 ## Step 3
231     my $tr = shift;
232 wakaba 1.9 $table->{row}->[$y_current] = {element => $tr};
233 wakaba 1.1 my @tdth = grep {
234     $_->node_type == 1 and
235     defined $_->namespace_uri and
236     $_->namespace_uri eq q<http://www.w3.org/1999/xhtml> and
237     {td => 1, th => 1}->{$_->manakai_local_name}
238     } @{$tr->child_nodes};
239 wakaba 1.7 my $current_cell = shift @tdth;
240    
241     ## Step 4
242     $growing_downward_growing_cells->();
243 wakaba 1.1
244 wakaba 1.9 return unless $current_cell;
245     ## ISSUE: Support for empty <tr></tr> (removed at revision 1376).
246    
247 wakaba 1.7 CELL: while (1) {
248     ## Step 5: cells
249 wakaba 1.1 $x_current++
250 wakaba 1.7 while ($x_current < $x_width and
251 wakaba 1.1 $table->{cell}->[$x_current]->[$y_current]);
252    
253 wakaba 1.7 ## Step 6
254     $x_width++ if $x_current == $x_width;
255    
256 wakaba 1.1 ## Step 7
257     my $colspan = 1;
258     my $attr_value = $current_cell->get_attribute_ns (undef, 'colspan');
259     if (defined $attr_value and $attr_value =~ /^[\x09-\x0D\x20]*([0-9]+)/) {
260     $colspan = $1 || 1;
261     }
262    
263 wakaba 1.7 ## Step 8
264 wakaba 1.1 my $rowspan = 1;
265     my $attr_value = $current_cell->get_attribute_ns (undef, 'rowspan');
266     if (defined $attr_value and $attr_value =~ /^[\x09-\x0D\x20]*([0-9]+)/) {
267     $rowspan = $1;
268     }
269    
270 wakaba 1.7 ## Step 9
271 wakaba 1.1 my $cell_grows_downward;
272     if ($rowspan == 0) {
273     $cell_grows_downward = 1;
274     $rowspan = 1;
275     }
276    
277 wakaba 1.7 ## Step 10
278     if ($x_width < $x_current + $colspan) {
279 wakaba 1.1 @column_generated_by[$_] = $current_cell
280 wakaba 1.7 for $x_width .. $x_current + $colspan - 1;
281     $x_width = $x_current + $colspan;
282 wakaba 1.1 }
283    
284 wakaba 1.7 ## Step 11
285     if ($y_height < $y_current + $rowspan) {
286 wakaba 1.9 @row_generated_by[$_] = $current_cell
287     for $y_height .. $y_current + $rowspan - 1;
288 wakaba 1.7 $y_height = $y_current + $rowspan;
289 wakaba 1.1 $y_max_node = $current_cell;
290     }
291    
292 wakaba 1.7 ## Step 12
293 wakaba 1.1 my $cell = {
294 wakaba 1.2 is_header => ($current_cell->manakai_local_name eq 'th'),
295 wakaba 1.1 element => $current_cell,
296     x => $x_current, y => $y_current,
297     width => $colspan, height => $rowspan,
298     };
299 wakaba 1.9 $column_has_anchored_cell[$x_current] = 1;
300     $row_has_anchored_cell[$y_current] = 1;
301 wakaba 1.1 for my $x ($x_current .. ($x_current + $colspan - 1)) {
302     for my $y ($y_current .. ($y_current + $rowspan - 1)) {
303     unless ($table->{cell}->[$x]->[$y]) {
304     $table->{cell}->[$x]->[$y] = [$cell];
305     } else {
306 wakaba 1.9 $onerror->(type => "cell overlapping:$x:$y", node => $current_cell,
307     level => $must_level);
308 wakaba 1.1 push @{$table->{cell}->[$x]->[$y]}, $cell;
309     }
310     }
311     }
312 wakaba 1.11
313     ## Whether the cell is an empty data cell or not
314     if (not $cell->{is_header}) {
315     $cell->{is_empty} = 1;
316     for my $node (@{$current_cell->child_nodes}) {
317     my $nt = $node->node_type;
318     if ($nt == 3 or $nt == 4) { # TEXT_NODE / CDATA_SECTION_NODE
319     if ($node->data =~ /\P{Zs}/) { ## TOOD: non-Zs class
320     delete $cell->{is_empty};
321     last;
322     }
323     } elsif ($nt == 1) { # ELEMENT_NODE
324     delete $cell->{is_empty};
325     last;
326     }
327     }
328     ## NOTE: Entity references are not supported
329     }
330 wakaba 1.1
331 wakaba 1.7 ## Step 13
332 wakaba 1.1 if ($cell_grows_downward) {
333     push @downward_growing_cells, [$cell, $x_current, $colspan];
334     }
335    
336 wakaba 1.7 ## Step 14
337 wakaba 1.1 $x_current += $colspan;
338 wakaba 1.7
339     ## Step 15-17
340     $current_cell = shift @tdth;
341     if (defined $current_cell) {
342     ## Step 16-17
343     #
344     } else {
345     ## Step 15
346     $y_current++;
347     last CELL;
348     }
349     } # CELL
350 wakaba 1.1 }; # $process_row
351    
352 wakaba 1.9 $process_row_group = sub ($) {
353 wakaba 1.8 ## Step 1
354     my $y_start = $y_height;
355    
356     ## Step 2
357     for (grep {
358     $_->node_type == 1 and
359     defined $_->namespace_uri and
360     $_->namespace_uri eq q<http://www.w3.org/1999/xhtml> and
361     $_->manakai_local_name eq 'tr'
362     } @{$_[0]->child_nodes}) {
363     $process_row->($_);
364     }
365    
366     ## Step 3
367     if ($y_height > $y_start) {
368     my $rg = {element => $current_element, ## ISSUE: "element being processed"?
369     x => 0, y => $y_start,
370     height => $y_height - $y_start};
371     $table->{row_group}->[$_] = $rg for $y_start .. $y_height - 1;
372     }
373    
374     ## Step 4
375     ## Ending a row group
376     ## Step 1
377     while ($y_current < $y_height) {
378     ## Step 1
379     $growing_downward_growing_cells->();
380    
381     ## Step 2
382     $y_current++;
383     }
384 wakaba 1.9 ## Step 2
385 wakaba 1.8 @downward_growing_cells = ();
386     }; # $process_row_group
387    
388 wakaba 1.10 ## Step 12: rows
389 wakaba 1.1 unshift @table_child, $current_element;
390     ROWS: {
391     NEXT_CHILD: {
392     $current_element = shift @table_child;
393     if (defined $current_element) {
394     redo NEXT_CHILD unless $current_element->node_type == 1;
395     my $nsuri = $current_element->namespace_uri;
396     redo NEXT_CHILD unless defined $nsuri and
397     $nsuri eq q<http://www.w3.org/1999/xhtml>;
398     $current_ln = $current_element->manakai_local_name;
399    
400     redo NEXT_CHILD unless {
401     thead => 1,
402     tbody => 1,
403     tfoot => 1,
404     tr => 1,
405     }->{$current_ln};
406     } else {
407 wakaba 1.8 ## Step 6 2nd paragraph
408 wakaba 1.9 $end->();
409 wakaba 1.11 $table->{width} = $x_width;
410     $table->{height} = $y_height;
411 wakaba 1.9 return $table;
412 wakaba 1.1 }
413     } # NEXT_CHILD
414    
415 wakaba 1.10 ## Step 13
416 wakaba 1.1 if ($current_ln eq 'tr') {
417     $process_row->($current_element);
418 wakaba 1.8 # advance (done at the first of ROWS)
419 wakaba 1.1 redo ROWS;
420     }
421    
422 wakaba 1.10 ## Step 14
423 wakaba 1.1 ## Ending a row group
424     ## Step 1
425 wakaba 1.7 while ($y_current < $y_height) {
426 wakaba 1.1 ## Step 1
427 wakaba 1.9 $growing_downward_growing_cells->();
428    
429     ## Step 2
430 wakaba 1.1 $y_current++;
431     }
432 wakaba 1.9 ## Step 2
433 wakaba 1.1 @downward_growing_cells = ();
434    
435 wakaba 1.10 ## Step 15
436 wakaba 1.8 if ($current_ln eq 'tfoot') {
437     push @$pending_tfoot, $current_element;
438     # advance (done at the top of ROWS)
439     redo ROWS;
440 wakaba 1.1 }
441    
442 wakaba 1.10 ## Step 16
443 wakaba 1.8 # thead or tbody
444     $process_row_group->($current_element);
445 wakaba 1.1
446 wakaba 1.10 ## Step 17
447 wakaba 1.8 # Advance (done at the top of ROWS).
448 wakaba 1.1
449 wakaba 1.10 ## Step 18
450 wakaba 1.8 redo ROWS;
451 wakaba 1.1 } # ROWS
452 wakaba 1.8
453 wakaba 1.9 $end->();
454 wakaba 1.11 $table->{width} = $x_width;
455     $table->{height} = $y_height;
456 wakaba 1.8 return $table;
457 wakaba 1.1 } # form_table
458    
459 wakaba 1.11 sub assign_header ($$;$$) {
460     my (undef, $table, $onerror, $must_level) = @_;
461     $onerror ||= sub { };
462     $must_level ||= 'm';
463    
464     my $assign_header = sub ($$$) {
465     my $_cell = shift;
466     my ($x, $y) = @_;
467    
468     for my $__cell (@{$_cell or []}) {
469     if ($__cell and $__cell->{element} and
470     not $__cell->{is_header} and
471     not $__cell->{element}->has_attribute_ns (undef, 'headers')) {
472     $__cell->{header}->{$x}->{$y} = 1;
473     }
474     }
475     }; # $assign_header
476    
477     for my $x (0 .. $table->{width} - 1) {
478     for my $y (0 .. $table->{height} - 1) {
479     my $cell = $table->{cell}->[$x]->[$y];
480     $cell = $cell->[0] if $cell; # anchored cell is always ->{cell}[][][0].
481     next if $cell->{x} != $x;
482     next if $cell->{y} != $y;
483     if ($cell) {
484     if ($cell->{is_header}) {
485     my $scope = $cell->{element}->get_attribute_ns (undef, 'scope');
486     $scope = $scope ? lc $scope : ''; ## TODO: case
487     if ($scope eq 'row') {
488     for my $_x ($x + $cell->{width} .. $table->{width} - 1) {
489     for my $_y ($y .. $y + $cell->{height} - 1) {
490     $assign_header->($table->{cell}->[$_x]->[$_y] => $x, $y);
491     }
492     }
493     } elsif ($scope eq 'col') {
494     for my $_x ($x .. $x + $cell->{width} - 1) {
495     for my $_y ($y .. $table->{height} - 1) {
496     $assign_header->($table->{cell}->[$_x]->[$_y] => $x, $y);
497     }
498     }
499     } elsif ($scope eq 'rowgroup') {
500     ## NOTE: A cell cannot exceed across a row group boundary.
501     if ($table->{row_group}->[$y] and
502     $table->{row_group}->[$y]->{height}) {
503     for my $_x ($x .. $table->{width} - 1) {
504     for my $_y ($y ..
505     $table->{row_group}->[$y]->{y} +
506     $table->{row_group}->[$y]->{height} - 1) {
507     $assign_header->($table->{cell}->[$_x]->[$_y] => $x, $y);
508     }
509     }
510     }
511     ## TODO: Should we raise a warning?
512     } elsif ($scope eq 'colgroup') {
513     if ($table->{column_group}->[$x] and
514     $table->{column_group}->{width} and
515     $table->{column_group}->[$x]->{x} == $x) { # anchored
516     for my $_x ($x ..
517     $table->{column_group}->[$x]->{x} +
518     $table->{column_group}->[$x]->{width} - 1) {
519     for my $_y ($y .. $table->{height} - 1) {
520     $assign_header->($table->{cell}->[$_x]->[$_y] => $x, $y);
521     }
522     }
523     }
524     ## TODO: Warning?
525     } else { # auto
526     ## 1.
527     my $header_width = $cell->{width};
528     W: for ($x + $cell->{width} .. $table->{width} - 1) {
529     my $_cell = $table->{cell}->[$_]->[$y];
530     for (@{$_cell or []}) {
531     if ($_->{element} and not $_->{is_empty}) {
532     last W; # not empty
533     }
534     }
535     $header_width++;
536     } # W
537    
538     ## 2.
539     my $_x = $x + $header_width;
540    
541     ## 3.
542     HORIZONTAL: {
543     last HORIZONTAL if $_x == $table->{width}; # goto Vertical
544    
545     ## 4. # goto Vertical
546     last HORIZONTAL
547     if $table->{cell}->[$_x]->[$y] and
548     $table->{cell}->[$_x]->[$y]->[0] and # anchored
549     $table->{cell}->[$_x]->[$y]->[0]->{is_header};
550    
551     ## 5.
552     for my $_y ($y .. $y + $cell->{height} - 1) {
553     $assign_header->($table->{cell}->[$_x]->[$_y] => $x, $y);
554     }
555    
556     ## 6.
557     $_x++;
558    
559     ## 7.
560     redo HORIZONTAL;
561     } # HORIZONTAL
562    
563     ## 8. Vertical
564     my $_y = $y + $cell->{height};
565    
566     VERTICAL: {
567     ## 9. # goto END
568     last VERTICAL if $_y == $table->{height};
569    
570     ## 10.
571     if ($table->{cell}->[$x]->[$_y]) {
572     my $h_cell = $table->{cell}->[$x]->[$_y]->[0]; # anchored cell
573     if ($h_cell and $h_cell->{is_header}) {
574     ## 10.1.
575     my $width = $h_cell->{width};
576     W: for ($h_cell->{x} + $width .. $table->{width} - 1) {
577     my $_cell = $table->{cell}->[$_]->[$y];
578     for (@{$_cell or []}) {
579     if ($_->{element} and not $_->{is_empty}) {
580     last W; # not empty
581     }
582     }
583     $width++;
584     } # W
585    
586     ## 10.2. # goto end
587     last VERTICAL if $width == $header_width;
588     } # 10.
589     }
590    
591     ## 11.
592     for my $_x ($x .. $x + $header_width - 1) {
593     $assign_header->($table->{cell}->[$_x]->[$_y] => $x, $y);
594     }
595    
596     ## 12.
597     $_y++;
598    
599     ## 13. # goto vertical (wrong)
600     redo VERTICAL;
601     } # VERTICAL
602    
603     ## 14. End
604     # (we have already done)
605     }
606     } else { # data cell
607    
608     }
609     }
610     }
611     }
612    
613    
614     ## NOTE: The "tree order" constraints in the spec algorithm are irrelevant
615     ## in fact.
616     } # assign_header
617 wakaba 1.1
618     1;
619 wakaba 1.11 ## $Date: 2008/05/05 08:36:55 $

[email protected]
ViewVC Help
Powered by ViewVC 1.1.24