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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.41 - (show annotations) (download)
Sat Aug 4 13:23:36 2007 UTC (19 years, 1 month ago) by wakaba
Branch: MAIN
Changes since 1.40: +130 -4 lines
++ whatpm/Whatpm/ChangeLog	4 Aug 2007 13:23:30 -0000
2007-08-04  Wakaba  <wakaba@suika.fam.cx>

	* ContentChecker.pm: HTML |time| element is implemented.

	* HTMLTable.pm: Comments are updated as HTML5 is revised.

1 package Whatpm::ContentChecker;
2 use strict;
3
4 require Whatpm::URIChecker;
5
6 ## ISSUE: How XML and XML Namespaces conformance can (or cannot)
7 ## be applied to an in-memory representation (i.e. DOM)?
8
9 my $XML_NS = q<http://www.w3.org/XML/1998/namespace>;
10 my $XMLNS_NS = q<http://www.w3.org/2000/xmlns/>;
11
12 my $AttrChecker = {
13 $XML_NS => {
14 space => sub {
15 my ($self, $attr) = @_;
16 my $value = $attr->value;
17 if ($value eq 'default' or $value eq 'preserve') {
18 #
19 } else {
20 ## NOTE: An XML "error"
21 $self->{onerror}->(node => $attr, level => 'error',
22 type => 'invalid attribute value');
23 }
24 },
25 lang => sub {
26 my ($self, $attr) = @_;
27 ## NOTE: "The values of the attribute are language identifiers
28 ## as defined by [IETF RFC 3066], Tags for the Identification
29 ## of Languages, or its successor; in addition, the empty string
30 ## may be specified." ("may" in lower case)
31 $self->{onerror}->(node => $attr, level => 'unsupported',
32 type => 'language tag');
33 if ($attr->owner_document->manakai_is_html) { # MUST NOT
34 $self->{onerror}->(node => $attr, type => 'in HTML:xml:lang');
35 ## TODO: Test data...
36 }
37 },
38 base => sub {
39 my ($self, $attr) = @_;
40 my $value = $attr->value;
41 if ($value =~ /[^\x{0000}-\x{10FFFF}]/) { ## ISSUE: Should we disallow noncharacters?
42 $self->{onerror}->(node => $attr,
43 type => 'invalid attribute value');
44 }
45 ## NOTE: Conformance to URI standard is not checked since there is
46 ## no author requirement on conformance in the XML Base specification.
47 },
48 id => sub {
49 my ($self, $attr) = @_;
50 my $value = $attr->value;
51 $value =~ s/[\x09\x0A\x0D\x20]+/ /g;
52 $value =~ s/^\x20//;
53 $value =~ s/\x20$//;
54 ## TODO: NCName in XML 1.0 or 1.1
55 ## TODO: declared type is ID?
56 if ($self->{id}->{$value}) { ## NOTE: An xml:id error
57 $self->{onerror}->(node => $attr, level => 'error',
58 type => 'duplicate ID');
59 push @{$self->{id}->{$value}}, $attr;
60 } else {
61 $self->{id}->{$value} = [$attr];
62 }
63 },
64 },
65 $XMLNS_NS => {
66 '' => sub {
67 my ($self, $attr) = @_;
68 my $ln = $attr->manakai_local_name;
69 my $value = $attr->value;
70 if ($value eq $XML_NS and $ln ne 'xml') {
71 $self->{onerror}
72 ->(node => $attr, level => 'NC',
73 type => 'Reserved Prefixes and Namespace Names:=xml');
74 } elsif ($value eq $XMLNS_NS) {
75 $self->{onerror}
76 ->(node => $attr, level => 'NC',
77 type => 'Reserved Prefixes and Namespace Names:=xmlns');
78 }
79 if ($ln eq 'xml' and $value ne $XML_NS) {
80 $self->{onerror}
81 ->(node => $attr, level => 'NC',
82 type => 'Reserved Prefixes and Namespace Names:xmlns:xml=');
83 } elsif ($ln eq 'xmlns') {
84 $self->{onerror}
85 ->(node => $attr, level => 'NC',
86 type => 'Reserved Prefixes and Namespace Names:xmlns:xmlns=');
87 }
88 ## TODO: If XML 1.0 and empty
89 },
90 xmlns => sub {
91 my ($self, $attr) = @_;
92 ## TODO: In XML 1.0, URI reference [RFC 3986] or an empty string
93 ## TODO: In XML 1.1, IRI reference [RFC 3987] or an empty string
94 ## TODO: relative references are deprecated
95 my $value = $attr->value;
96 if ($value eq $XML_NS) {
97 $self->{onerror}
98 ->(node => $attr, level => 'NC',
99 type => 'Reserved Prefixes and Namespace Names:=xml');
100 } elsif ($value eq $XMLNS_NS) {
101 $self->{onerror}
102 ->(node => $attr, level => 'NC',
103 type => 'Reserved Prefixes and Namespace Names:=xmlns');
104 }
105 },
106 },
107 };
108
109 ## ISSUE: Should we really allow these attributes?
110 $AttrChecker->{''}->{'xml:space'} = $AttrChecker->{$XML_NS}->{space};
111 $AttrChecker->{''}->{'xml:lang'} = $AttrChecker->{$XML_NS}->{lang};
112 $AttrChecker->{''}->{'xml:base'} = $AttrChecker->{$XML_NS}->{base};
113 $AttrChecker->{''}->{'xml:id'} = $AttrChecker->{$XML_NS}->{id};
114
115 ## ANY
116 my $AnyChecker = sub {
117 my ($self, $todo) = @_;
118 my $el = $todo->{node};
119 my $new_todos = [];
120 my @nodes = (@{$el->child_nodes});
121 while (@nodes) {
122 my $node = shift @nodes;
123 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
124
125 my $nt = $node->node_type;
126 if ($nt == 1) {
127 my $node_ns = $node->namespace_uri;
128 $node_ns = '' unless defined $node_ns;
129 my $node_ln = $node->manakai_local_name;
130 if ($self->{minuses}->{$node_ns}->{$node_ln}) {
131 $self->{onerror}->(node => $node, type => 'element not allowed');
132 }
133 push @$new_todos, {type => 'element', node => $node};
134 } elsif ($nt == 5) {
135 unshift @nodes, @{$node->child_nodes};
136 }
137 }
138 return ($new_todos);
139 }; # $AnyChecker
140
141 my $ElementDefault = {
142 checker => sub {
143 my ($self, $todo) = @_;
144 $self->{onerror}->(node => $todo->{node}, level => 'unsupported',
145 type => 'element');
146 return $AnyChecker->($self, $todo);
147 },
148 attrs_checker => sub {
149 my ($self, $todo) = @_;
150 for my $attr (@{$todo->{node}->attributes}) {
151 my $attr_ns = $attr->namespace_uri;
152 $attr_ns = '' unless defined $attr_ns;
153 my $attr_ln = $attr->manakai_local_name;
154 my $checker = $AttrChecker->{$attr_ns}->{$attr_ln}
155 || $AttrChecker->{$attr_ns}->{''};
156 if ($checker) {
157 $checker->($self, $attr);
158 } else {
159 $self->{onerror}->(node => $attr, level => 'unsupported',
160 type => 'attribute');
161 }
162 }
163 },
164 };
165
166 my $Element = {};
167
168 my $HTML_NS = q<http://www.w3.org/1999/xhtml>;
169
170 my $HTMLMetadataElements = {
171 $HTML_NS => {
172 qw/link 1 meta 1 style 1 script 1 event-source 1 command 1 base 1 title 1
173 noscript 1
174 /,
175 },
176 };
177
178 my $HTMLSectioningElements = {
179 $HTML_NS => {qw/body 1 section 1 nav 1 article 1 blockquote 1 aside 1/},
180 };
181
182 my $HTMLBlockLevelElements = {
183 $HTML_NS => {
184 qw/
185 section 1 nav 1 article 1 blockquote 1 aside 1
186 h1 1 h2 1 h3 1 h4 1 h5 1 h6 1 header 1 footer 1
187 address 1 p 1 hr 1 dialog 1 pre 1 ol 1 ul 1 dl 1
188 ins 1 del 1 figure 1 map 1 table 1 script 1 noscript 1
189 event-source 1 details 1 datagrid 1 menu 1 div 1 font 1
190 /,
191 },
192 };
193
194 my $HTMLStrictlyInlineLevelElements = {
195 $HTML_NS => {
196 qw/
197 br 1 a 1 q 1 cite 1 em 1 strong 1 small 1 m 1 dfn 1 abbr 1
198 time 1 meter 1 progress 1 code 1 var 1 samp 1 kbd 1
199 sub 1 sup 1 span 1 i 1 b 1 bdo 1 ins 1 del 1 img 1
200 iframe 1 embed 1 object 1 video 1 audio 1 canvas 1 area 1
201 script 1 noscript 1 event-source 1 command 1 font 1
202 /,
203 },
204 };
205
206 my $HTMLStructuredInlineLevelElements = {
207 $HTML_NS => {qw/blockquote 1 pre 1 ol 1 ul 1 dl 1 table 1 menu 1/},
208 };
209
210 my $HTMLInteractiveElements = {
211 $HTML_NS => {a => 1, details => 1, datagrid => 1},
212 };
213 ## NOTE: |html:a| and |html:datagrid| are not allowed as a descendant
214 ## of interactive elements
215
216 my $HTMLTransparentElements = {
217 $HTML_NS => {qw/ins 1 font 1 noscript 1/},
218 ## NOTE: |html:noscript| is transparent if scripting is disabled
219 ## and not in |head|.
220 };
221
222 #my $HTMLSemiTransparentElements = {
223 # $HTML_NS => {qw/video 1 audio 1/},
224 #};
225
226 my $HTMLEmbededElements = {
227 $HTML_NS => {qw/img 1 iframe 1 embed 1 object 1 video 1 audio 1 canvas 1/},
228 };
229
230 ## Empty
231 my $HTMLEmptyChecker = sub {
232 my ($self, $todo) = @_;
233 my $el = $todo->{node};
234 my $new_todos = [];
235 my @nodes = (@{$el->child_nodes});
236
237 while (@nodes) {
238 my $node = shift @nodes;
239 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
240
241 my $nt = $node->node_type;
242 if ($nt == 1) {
243 ## NOTE: |minuses| list is not checked since redundant
244 $self->{onerror}->(node => $node, type => 'element not allowed');
245 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
246 unshift @nodes, @$sib;
247 push @$new_todos, @$ch;
248 } elsif ($nt == 3 or $nt == 4) {
249 if ($node->data =~ /[^\x09-\x0D\x20]/) {
250 $self->{onerror}->(node => $node, type => 'character not allowed');
251 }
252 } elsif ($nt == 5) {
253 unshift @nodes, @{$node->child_nodes};
254 }
255 }
256 return ($new_todos);
257 };
258
259 ## Text
260 my $HTMLTextChecker = sub {
261 my ($self, $todo) = @_;
262 my $el = $todo->{node};
263 my $new_todos = [];
264 my @nodes = (@{$el->child_nodes});
265
266 while (@nodes) {
267 my $node = shift @nodes;
268 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
269
270 my $nt = $node->node_type;
271 if ($nt == 1) {
272 ## NOTE: |minuses| list is not checked since redundant
273 $self->{onerror}->(node => $node, type => 'element not allowed');
274 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
275 unshift @nodes, @$sib;
276 push @$new_todos, @$ch;
277 } elsif ($nt == 5) {
278 unshift @nodes, @{$node->child_nodes};
279 }
280 }
281 return ($new_todos);
282 };
283
284 ## Zero or more |html:style| elements,
285 ## followed by zero or more block-level elements
286 my $HTMLStylableBlockChecker = sub {
287 my ($self, $todo) = @_;
288 my $el = $todo->{node};
289 my $new_todos = [];
290 my @nodes = (@{$el->child_nodes});
291
292 my $has_non_style;
293 while (@nodes) {
294 my $node = shift @nodes;
295 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
296
297 my $nt = $node->node_type;
298 if ($nt == 1) {
299 my $node_ns = $node->namespace_uri;
300 $node_ns = '' unless defined $node_ns;
301 my $node_ln = $node->manakai_local_name;
302 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
303 if ($node_ns eq $HTML_NS and $node_ln eq 'style') {
304 $not_allowed = 1 if $has_non_style or
305 not $node->has_attribute_ns (undef, 'scoped');
306 } elsif ($HTMLBlockLevelElements->{$node_ns}->{$node_ln}) {
307 $has_non_style = 1;
308 } else {
309 $has_non_style = 1;
310 $not_allowed = 1;
311 }
312 $self->{onerror}->(node => $node, type => 'element not allowed')
313 if $not_allowed;
314 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
315 unshift @nodes, @$sib;
316 push @$new_todos, @$ch;
317 } elsif ($nt == 3 or $nt == 4) {
318 if ($node->data =~ /[^\x09-\x0D\x20]/) {
319 $self->{onerror}->(node => $node, type => 'character not allowed');
320 }
321 } elsif ($nt == 5) {
322 unshift @nodes, @{$node->child_nodes};
323 }
324 }
325 return ($new_todos);
326 }; # $HTMLStylableBlockChecker
327
328 ## Zero or more block-level elements
329 my $HTMLBlockChecker = sub {
330 my ($self, $todo) = @_;
331 my $el = $todo->{node};
332 my $new_todos = [];
333 my @nodes = (@{$el->child_nodes});
334
335 while (@nodes) {
336 my $node = shift @nodes;
337 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
338
339 my $nt = $node->node_type;
340 if ($nt == 1) {
341 my $node_ns = $node->namespace_uri;
342 $node_ns = '' unless defined $node_ns;
343 my $node_ln = $node->manakai_local_name;
344 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
345 $not_allowed = 1
346 unless $HTMLBlockLevelElements->{$node_ns}->{$node_ln};
347 $self->{onerror}->(node => $node, type => 'element not allowed')
348 if $not_allowed;
349 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
350 unshift @nodes, @$sib;
351 push @$new_todos, @$ch;
352 } elsif ($nt == 3 or $nt == 4) {
353 if ($node->data =~ /[^\x09-\x0D\x20]/) {
354 $self->{onerror}->(node => $node, type => 'character not allowed');
355 }
356 } elsif ($nt == 5) {
357 unshift @nodes, @{$node->child_nodes};
358 }
359 }
360 return ($new_todos);
361 }; # $HTMLBlockChecker
362
363 ## Inline-level content
364 my $HTMLInlineChecker = sub {
365 my ($self, $todo) = @_;
366 my $el = $todo->{node};
367 my $new_todos = [];
368 my @nodes = (@{$el->child_nodes});
369
370 while (@nodes) {
371 my $node = shift @nodes;
372 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
373
374 my $nt = $node->node_type;
375 if ($nt == 1) {
376 my $node_ns = $node->namespace_uri;
377 $node_ns = '' unless defined $node_ns;
378 my $node_ln = $node->manakai_local_name;
379 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
380 $not_allowed = 1
381 unless $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln} or
382 $HTMLStructuredInlineLevelElements->{$node_ns}->{$node_ln};
383 $self->{onerror}->(node => $node, type => 'element not allowed')
384 if $not_allowed;
385 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
386 unshift @nodes, @$sib;
387 push @$new_todos, @$ch;
388 } elsif ($nt == 5) {
389 unshift @nodes, @{$node->child_nodes};
390 }
391 }
392
393 for (@$new_todos) {
394 $_->{inline} = 1;
395 }
396 return ($new_todos);
397 }; # $HTMLInlineChecker
398
399 my $HTMLSignificantInlineChecker = $HTMLInlineChecker;
400 ## TODO: check significant content
401
402 ## Strictly inline-level content
403 my $HTMLStrictlyInlineChecker = sub {
404 my ($self, $todo) = @_;
405 my $el = $todo->{node};
406 my $new_todos = [];
407 my @nodes = (@{$el->child_nodes});
408
409 while (@nodes) {
410 my $node = shift @nodes;
411 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
412
413 my $nt = $node->node_type;
414 if ($nt == 1) {
415 my $node_ns = $node->namespace_uri;
416 $node_ns = '' unless defined $node_ns;
417 my $node_ln = $node->manakai_local_name;
418 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
419 $not_allowed = 1
420 unless $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln};
421 $self->{onerror}->(node => $node, type => 'element not allowed')
422 if $not_allowed;
423 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
424 unshift @nodes, @$sib;
425 push @$new_todos, @$ch;
426 } elsif ($nt == 5) {
427 unshift @nodes, @{$node->child_nodes};
428 }
429 }
430
431 for (@$new_todos) {
432 $_->{inline} = 1;
433 $_->{strictly_inline} = 1;
434 }
435 return ($new_todos);
436 }; # $HTMLStrictlyInlineChecker
437
438 my $HTMLSignificantStrictlyInlineChecker = $HTMLStrictlyInlineChecker;
439 ## TODO: check significant content
440
441 ## Inline-level or strictly inline-kevek content
442 my $HTMLInlineOrStrictlyInlineChecker = sub {
443 my ($self, $todo) = @_;
444 my $el = $todo->{node};
445 my $new_todos = [];
446 my @nodes = (@{$el->child_nodes});
447
448 while (@nodes) {
449 my $node = shift @nodes;
450 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
451
452 my $nt = $node->node_type;
453 if ($nt == 1) {
454 my $node_ns = $node->namespace_uri;
455 $node_ns = '' unless defined $node_ns;
456 my $node_ln = $node->manakai_local_name;
457 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
458 if ($todo->{strictly_inline}) {
459 $not_allowed = 1
460 unless $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln};
461 } else {
462 $not_allowed = 1
463 unless $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln} or
464 $HTMLStructuredInlineLevelElements->{$node_ns}->{$node_ln};
465 }
466 $self->{onerror}->(node => $node, type => 'element not allowed')
467 if $not_allowed;
468 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
469 unshift @nodes, @$sib;
470 push @$new_todos, @$ch;
471 } elsif ($nt == 5) {
472 unshift @nodes, @{$node->child_nodes};
473 }
474 }
475
476 for (@$new_todos) {
477 $_->{inline} = 1;
478 $_->{strictly_inline} = 1;
479 }
480 return ($new_todos);
481 }; # $HTMLInlineOrStrictlyInlineChecker
482
483 my $HTMLSignificantInlineOrStrictlyInlineChecker
484 = $HTMLInlineOrStrictlyInlineChecker;
485 ## TODO: check significant content
486
487 my $HTMLBlockOrInlineChecker = sub {
488 my ($self, $todo) = @_;
489 my $el = $todo->{node};
490 my $new_todos = [];
491 my @nodes = (@{$el->child_nodes});
492
493 my $content = 'block-or-inline'; # or 'block' or 'inline'
494 my @block_not_inline;
495 while (@nodes) {
496 my $node = shift @nodes;
497 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
498
499 my $nt = $node->node_type;
500 if ($nt == 1) {
501 my $node_ns = $node->namespace_uri;
502 $node_ns = '' unless defined $node_ns;
503 my $node_ln = $node->manakai_local_name;
504 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
505 if ($content eq 'block') {
506 $not_allowed = 1
507 unless $HTMLBlockLevelElements->{$node_ns}->{$node_ln};
508 } elsif ($content eq 'inline') {
509 $not_allowed = 1
510 unless $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln} or
511 $HTMLStructuredInlineLevelElements->{$node_ns}->{$node_ln};
512 } else {
513 my $is_block = $HTMLBlockLevelElements->{$node_ns}->{$node_ln};
514 my $is_inline
515 = $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln} ||
516 $HTMLStructuredInlineLevelElements->{$node_ns}->{$node_ln};
517
518 push @block_not_inline, $node
519 if $is_block and not $is_inline and not $not_allowed;
520 unless ($is_block) {
521 $content = 'inline';
522 for (@block_not_inline) {
523 $self->{onerror}->(node => $_, type => 'element not allowed');
524 }
525 $not_allowed = 1 unless $is_inline;
526 }
527 }
528 $self->{onerror}->(node => $node, type => 'element not allowed')
529 if $not_allowed;
530 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
531 unshift @nodes, @$sib;
532 push @$new_todos, @$ch;
533 } elsif ($nt == 3 or $nt == 4) {
534 if ($node->data =~ /[^\x09-\x0D\x20]/) {
535 if ($content eq 'block') {
536 $self->{onerror}->(node => $node, type => 'character not allowed');
537 } else {
538 $content = 'inline';
539 for (@block_not_inline) {
540 $self->{onerror}->(node => $_, type => 'element not allowed');
541 }
542 }
543 }
544 } elsif ($nt == 5) {
545 unshift @nodes, @{$node->child_nodes};
546 }
547 }
548
549 if ($content eq 'inline') {
550 for (@$new_todos) {
551 $_->{inline} = 1;
552 }
553 }
554 return ($new_todos);
555 };
556
557 ## Zero or more XXX element, then either block-level or inline-level
558 my $GetHTMLZeroOrMoreThenBlockOrInlineChecker = sub ($$) {
559 my ($elnsuri, $ellname) = @_;
560 return sub {
561 my ($self, $todo) = @_;
562 my $el = $todo->{node};
563 my $new_todos = [];
564 my @nodes = (@{$el->child_nodes});
565
566 my $has_non_style;
567 my $content = 'block-or-inline'; # or 'block' or 'inline'
568 my @block_not_inline;
569 while (@nodes) {
570 my $node = shift @nodes;
571 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
572
573 my $nt = $node->node_type;
574 if ($nt == 1) {
575 my $node_ns = $node->namespace_uri;
576 $node_ns = '' unless defined $node_ns;
577 my $node_ln = $node->manakai_local_name;
578 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
579 if ($node_ns eq $elnsuri and $node_ln eq $ellname) {
580 $not_allowed = 1 if $has_non_style;
581 if ($ellname eq 'style' and
582 not $node->has_attribute_ns (undef, 'scoped')) {
583 $not_allowed = 1;
584 }
585 } elsif ($content eq 'block') {
586 $has_non_style = 1;
587 $not_allowed = 1
588 unless $HTMLBlockLevelElements->{$node_ns}->{$node_ln};
589 } elsif ($content eq 'inline') {
590 $has_non_style = 1;
591 $not_allowed = 1
592 unless $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln} or
593 $HTMLStructuredInlineLevelElements->{$node_ns}->{$node_ln};
594 } else {
595 $has_non_style = 1;
596 my $is_block = $HTMLBlockLevelElements->{$node_ns}->{$node_ln};
597 my $is_inline
598 = $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln} ||
599 $HTMLStructuredInlineLevelElements->{$node_ns}->{$node_ln};
600
601 push @block_not_inline, $node
602 if $is_block and not $is_inline and not $not_allowed;
603 unless ($is_block) {
604 $content = 'inline';
605 for (@block_not_inline) {
606 $self->{onerror}->(node => $_, type => 'element not allowed');
607 }
608 $not_allowed = 1 unless $is_inline;
609 }
610 }
611 $self->{onerror}->(node => $node, type => 'element not allowed')
612 if $not_allowed;
613 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
614 unshift @nodes, @$sib;
615 push @$new_todos, @$ch;
616 } elsif ($nt == 3 or $nt == 4) {
617 if ($node->data =~ /[^\x09-\x0D\x20]/) {
618 $has_non_style = 1;
619 if ($content eq 'block') {
620 $self->{onerror}->(node => $node, type => 'character not allowed');
621 } else {
622 $content = 'inline';
623 for (@block_not_inline) {
624 $self->{onerror}->(node => $_, type => 'element not allowed');
625 }
626 }
627 }
628 } elsif ($nt == 5) {
629 unshift @nodes, @{$node->child_nodes};
630 }
631 }
632
633 if ($content eq 'inline') {
634 for (@$new_todos) {
635 $_->{inline} = 1;
636 }
637 }
638 return ($new_todos);
639 };
640 }; # $GetHTMLZeroOrMoreThenBlockOrInlineChecker
641
642 my $HTMLTransparentChecker = $HTMLBlockOrInlineChecker;
643
644 my $GetHTMLEnumeratedAttrChecker = sub {
645 my $states = shift; # {value => conforming ? 1 : -1}
646 return sub {
647 my ($self, $attr) = @_;
648 my $value = lc $attr->value; ## TODO: ASCII case insensitibility?
649 if ($states->{$value} > 0) {
650 #
651 } elsif ($states->{$value}) {
652 $self->{onerror}->(node => $attr, type => 'enumerated:non-conforming');
653 } else {
654 $self->{onerror}->(node => $attr, type => 'enumerated:invalid');
655 }
656 };
657 }; # $GetHTMLEnumeratedAttrChecker
658
659 my $GetHTMLBooleanAttrChecker = sub {
660 my $local_name = shift;
661 return sub {
662 my ($self, $attr) = @_;
663 my $value = $attr->value;
664 unless ($value eq $local_name or $value eq '') {
665 $self->{onerror}->(node => $attr, type => 'boolean:invalid');
666 }
667 };
668 }; # $GetHTMLBooleanAttrChecker
669
670 ## |rel| attribute (unordered set of space separated tokens,
671 ## whose allowed values are defined by the section on link types)
672 my $HTMLLinkTypesAttrChecker = sub {
673 my ($a_or_area, $self, $attr) = @_;
674 my %word;
675 for my $word (grep {length $_} split /[\x09-\x0D\x20]/, $attr->value) {
676 unless ($word{$word}) {
677 $word{$word} = 1;
678 } else {
679 $self->{onerror}->(node => $attr, type => 'duplicate token:'.$word);
680 }
681 }
682 ## NOTE: Case sensitive match (since HTML5 spec does not say link
683 ## types are case-insensitive and it says "The value should not
684 ## be confusingly similar to any other defined value (e.g.
685 ## differing only in case).").
686 ## NOTE: Though there is no explicit "MUST NOT" for undefined values,
687 ## "MAY"s and "only ... MAY" restrict non-standard non-registered
688 ## values to be used conformingly.
689 require Whatpm::_LinkTypeList;
690 our $LinkType;
691 for my $word (keys %word) {
692 my $def = $LinkType->{$word};
693 if (defined $def) {
694 if ($def->{status} eq 'accepted') {
695 if (defined $def->{effect}->[$a_or_area]) {
696 #
697 } else {
698 $self->{onerror}->(node => $attr,
699 type => 'link type:bad context:'.$word);
700 }
701 } elsif ($def->{status} eq 'proposal') {
702 $self->{onerror}->(node => $attr, level => 's',
703 type => 'link type:proposed:'.$word);
704 } else { # rejected or synonym
705 $self->{onerror}->(node => $attr,
706 type => 'link type:non-conforming:'.$word);
707 }
708 if ($def->{unique}) {
709 unless ($self->{has_link_type}->{$word}) {
710 $self->{has_link_type}->{$word} = 1;
711 } else {
712 $self->{onerror}->(node => $attr,
713 type => 'link type:duplicate:'.$word);
714 }
715 }
716 } else {
717 $self->{onerror}->(node => $attr, level => 'unsupported',
718 type => 'link type:'.$word);
719 }
720 }
721 ## TODO: The Pingback 1.0 specification, which is referenced by HTML5,
722 ## says that using both X-Pingback: header field and HTML
723 ## <link rel=pingback> is deprecated and if both appears they
724 ## SHOULD contain exactly the same value.
725 ## ISSUE: Pingback 1.0 specification defines the exact representation
726 ## of its link element, which cannot be tested by the current arch.
727 ## ISSUE: Pingback 1.0 specification says that the document MUST NOT
728 ## include any string that matches to the pattern for the rel=pingback link,
729 ## which again inpossible to test.
730 ## ISSUE: rel=pingback href MUST NOT include entities other than predefined 4.
731 }; # $HTMLLinkTypesAttrChecker
732
733 ## URI (or IRI)
734 my $HTMLURIAttrChecker = sub {
735 my ($self, $attr) = @_;
736 ## ISSUE: Relative references are allowed? (RFC 3987 "IRI" is an absolute reference with optional fragment identifier.)
737 my $value = $attr->value;
738 Whatpm::URIChecker->check_iri_reference ($value, sub {
739 my %opt = @_;
740 $self->{onerror}->(node => $attr, level => $opt{level},
741 type => 'URI::'.$opt{type}.
742 (defined $opt{position} ? ':'.$opt{position} : ''));
743 });
744 }; # $HTMLURIAttrChecker
745
746 ## A space separated list of one or more URIs (or IRIs)
747 my $HTMLSpaceURIsAttrChecker = sub {
748 my ($self, $attr) = @_;
749 my $i = 0;
750 for my $value (split /[\x09-\x0D\x20]+/, $attr->value) {
751 Whatpm::URIChecker->check_iri_reference ($value, sub {
752 my %opt = @_;
753 $self->{onerror}->(node => $attr, level => $opt{level},
754 type => 'URIs:'.$i.'::'.
755 $opt{type}.
756 (defined $opt{position} ? ':'.$opt{position} : ''));
757 });
758 $i++;
759 }
760 ## ISSUE: Relative references?
761 ## ISSUE: Leading or trailing white spaces are conformant?
762 ## ISSUE: A sequence of white space characters are conformant?
763 ## ISSUE: A zero-length string is conformant? (It does contain a relative reference, i.e. same as base URI.)
764 ## NOTE: Duplication seems not an error.
765 }; # $HTMLSpaceURIsAttrChecker
766
767 my $HTMLDatetimeAttrChecker = sub {
768 my ($self, $attr) = @_;
769 my $value = $attr->value;
770 ## ISSUE: "space", not "space character" (in parsing algorihtm, "space character")
771 if ($value =~ /\A([0-9]{4})-([0-9]{2})-([0-9]{2})(?>[\x09-\x0D\x20]+(?>T[\x09-\x0D\x20]*)?|T[\x09-\x0D\x20]*)([0-9]{2}):([0-9]{2})(?>:([0-9]{2}))?(?>\.([0-9]+))?[\x09-\x0D\x20]*(?>Z|[+-]([0-9]{2}):([0-9]{2}))\z/) {
772 my ($y, $M, $d, $h, $m, $s, $f, $zh, $zm)
773 = ($1, $2, $3, $4, $5, $6, $7, $8, $9);
774 if (0 < $M and $M < 13) { ## ISSUE: This is not explicitly specified (though in parsing algorithm)
775 $self->{onerror}->(node => $attr, type => 'datetime:bad day')
776 if $d < 1 or
777 $d > [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]->[$M];
778 $self->{onerror}->(node => $attr, type => 'datetime:bad day')
779 if $M == 2 and $d == 29 and
780 not ($y % 400 == 0 or ($y % 4 == 0 and $y % 100 != 0));
781 } else {
782 $self->{onerror}->(node => $attr, type => 'datetime:bad month');
783 }
784 $self->{onerror}->(node => $attr, type => 'datetime:bad hour') if $h > 23;
785 $self->{onerror}->(node => $attr, type => 'datetime:bad minute') if $m > 59;
786 $self->{onerror}->(node => $attr, type => 'datetime:bad second')
787 if defined $s and $s > 59;
788 $self->{onerror}->(node => $attr, type => 'datetime:bad timezone hour')
789 if $zh > 23;
790 $self->{onerror}->(node => $attr, type => 'datetime:bad timezone minute')
791 if $zm > 59;
792 ## ISSUE: Maybe timezone -00:00 should have same semantics as in RFC 3339.
793 } else {
794 $self->{onerror}->(node => $attr, type => 'datetime:syntax error');
795 }
796 }; # $HTMLDatetimeAttrChecker
797
798 my $HTMLIntegerAttrChecker = sub {
799 my ($self, $attr) = @_;
800 my $value = $attr->value;
801 unless ($value =~ /\A-?[0-9]+\z/) {
802 $self->{onerror}->(node => $attr, type => 'integer:syntax error');
803 }
804 }; # $HTMLIntegerAttrChecker
805
806 my $GetHTMLNonNegativeIntegerAttrChecker = sub {
807 my $range_check = shift;
808 return sub {
809 my ($self, $attr) = @_;
810 my $value = $attr->value;
811 if ($value =~ /\A[0-9]+\z/) {
812 unless ($range_check->($value + 0)) {
813 $self->{onerror}->(node => $attr, type => 'nninteger:out of range');
814 }
815 } else {
816 $self->{onerror}->(node => $attr,
817 type => 'nninteger:syntax error');
818 }
819 };
820 }; # $GetHTMLNonNegativeIntegerAttrChecker
821
822 my $GetHTMLFloatingPointNumberAttrChecker = sub {
823 my $range_check = shift;
824 return sub {
825 my ($self, $attr) = @_;
826 my $value = $attr->value;
827 if ($value =~ /\A-?[0-9.]+\z/ and $value =~ /[0-9]/) {
828 unless ($range_check->($value + 0)) {
829 $self->{onerror}->(node => $attr, type => 'float:out of range');
830 }
831 } else {
832 $self->{onerror}->(node => $attr,
833 type => 'float:syntax error');
834 }
835 };
836 }; # $GetHTMLFloatingPointNumberAttrChecker
837
838 ## "A valid MIME type, optionally with parameters. [RFC 2046]"
839 ## ISSUE: RFC 2046 does not define syntax of media types.
840 ## ISSUE: The definition of "a valid MIME type" is unknown.
841 ## Syntactical correctness?
842 my $HTMLIMTAttrChecker = sub {
843 my ($self, $attr) = @_;
844 my $value = $attr->value;
845 ## ISSUE: RFC 2045 Content-Type header field allows insertion
846 ## of LWS/comments between tokens. Is it allowed in HTML? Maybe no.
847 ## ISSUE: RFC 2231 extension? Maybe no.
848 my $lws0 = qr/(?>(?>\x0D\x0A)?[\x09\x20])*/;
849 my $token = qr/[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+/;
850 my $qs = qr/"(?>[\x00-\x0C\x0E-\x21\x23-\x5B\x5D-\x7E]|\x0D\x0A[\x09\x20]|\x5C[\x00-\x7F])*"/;
851 if ($value =~ m#\A$lws0($token)$lws0/$lws0($token)$lws0((?>;$lws0$token$lws0=$lws0(?>$token|$qs)$lws0)*)\z#) {
852 my @type = ($1, $2);
853 my $param = $3;
854 while ($param =~ s/^;$lws0($token)$lws0=$lws0(?>($token)|($qs))$lws0//) {
855 if (defined $2) {
856 push @type, $1 => $2;
857 } else {
858 my $n = $1;
859 my $v = $2;
860 $v =~ s/\\(.)/$1/gs;
861 push @type, $n => $v;
862 }
863 }
864 require Whatpm::IMTChecker;
865 Whatpm::IMTChecker->check_imt (sub {
866 my %opt = @_;
867 $self->{onerror}->(node => $attr, level => $opt{level},
868 type => 'IMT:'.$opt{type});
869 }, @type);
870 } else {
871 $self->{onerror}->(node => $attr, type => 'IMT:syntax error');
872 }
873 }; # $HTMLIMTAttrChecker
874
875 my $HTMLLanguageTagAttrChecker = sub {
876 my ($self, $attr) = @_;
877 $self->{onerror}->(node => $attr, level => 'unsupported',
878 type => 'language tag');
879 if ($attr->value eq '') {
880 $self->{onerror}->(node => $attr, type => 'language tag:syntax error');
881 }
882 ## TODO: RFC 3066 test
883 ## ISSUE: RFC 4646 (3066bis)?
884 }; # $HTMLLanguageTagAttrChecker
885
886 ## "A valid media query [MQ]"
887 my $HTMLMQAttrChecker = sub {
888 my ($self, $attr) = @_;
889 $self->{onerror}->(node => $attr, level => 'unsupported',
890 type => 'media query');
891 ## ISSUE: What is "a valid media query"?
892 }; # $HTMLMQAttrChecker
893
894 my $HTMLEventHandlerAttrChecker = sub {
895 my ($self, $attr) = @_;
896 $self->{onerror}->(node => $attr, level => 'unsupported',
897 type => 'event handler');
898 ## TODO: MUST contain valid ECMAScript code matching the
899 ## ECMAScript |FunctionBody| production. [ECMA262]
900 ## ISSUE: MUST be ES3? E4X? ES4? JS1.x?
901 ## ISSUE: Automatic semicolon insertion does not apply?
902 ## ISSUE: Other script languages?
903 }; # $HTMLEventHandlerAttrChecker
904
905 my $HTMLUsemapAttrChecker = sub {
906 my ($self, $attr) = @_;
907 ## MUST be a valid hashed ID reference to a |map| element
908 my $value = $attr->value;
909 if ($value =~ s/^#//) {
910 ## ISSUE: Is |usemap="#"| conformant? (c.f. |id=""| is non-conformant.)
911 push @{$self->{usemap}}, [$value => $attr];
912 } else {
913 $self->{onerror}->(node => $attr, type => '#idref:syntax error');
914 }
915 ## NOTE: Space characters in hashed ID references are conforming.
916 ## ISSUE: UA algorithm for matching is case-insensitive; IDs only different in cases should be reported
917 }; # $HTMLUsemapAttrChecker
918
919 my $HTMLTargetAttrChecker = sub {
920 my ($self, $attr) = @_;
921 my $value = $attr->value;
922 if ($value =~ /^_/) {
923 $value = lc $value; ## ISSUE: ASCII case-insentitive?
924 unless ({
925 _self => 1, _parent => 1, _top => 1,
926 }->{$value}) {
927 $self->{onerror}->(node => $attr,
928 type => 'reserved browsing context name');
929 }
930 } else {
931 #$ ISSUE: An empty string is conforming?
932 }
933 }; # $HTMLTargetAttrChecker
934
935 my $HTMLAttrChecker = {
936 id => sub {
937 ## NOTE: |map| has its own variant of |id=""| checker
938 my ($self, $attr) = @_;
939 my $value = $attr->value;
940 if (length $value > 0) {
941 if ($self->{id}->{$value}) {
942 $self->{onerror}->(node => $attr, type => 'duplicate ID');
943 push @{$self->{id}->{$value}}, $attr;
944 } else {
945 $self->{id}->{$value} = [$attr];
946 }
947 if ($value =~ /[\x09-\x0D\x20]/) {
948 $self->{onerror}->(node => $attr, type => 'space in ID');
949 }
950 } else {
951 ## NOTE: MUST contain at least one character
952 $self->{onerror}->(node => $attr, type => 'empty attribute value');
953 }
954 },
955 title => sub {}, ## NOTE: No conformance creteria
956 lang => sub {
957 my ($self, $attr) = @_;
958 $self->{onerror}->(node => $attr, level => 'unsupported',
959 type => 'language tag');
960 ## TODO: RFC 3066 or empty test
961 ## ISSUE: RFC 4646 (3066bis)?
962 unless ($attr->owner_document->manakai_is_html) {
963 $self->{onerror}->(node => $attr, type => 'in XML:lang');
964 }
965 },
966 dir => $GetHTMLEnumeratedAttrChecker->({ltr => 1, rtl => 1}),
967 class => sub {
968 my ($self, $attr) = @_;
969 my %word;
970 for my $word (grep {length $_} split /[\x09-\x0D\x20]/, $attr->value) {
971 unless ($word{$word}) {
972 $word{$word} = 1;
973 push @{$self->{return}->{class}->{$word}||=[]}, $attr;
974 } else {
975 $self->{onerror}->(node => $attr, type => 'duplicate token:'.$word);
976 }
977 }
978 },
979 contextmenu => sub {
980 my ($self, $attr) = @_;
981 my $value = $attr->value;
982 push @{$self->{contextmenu}}, [$value => $attr];
983 ## ISSUE: "The value must be the ID of a menu element in the DOM."
984 ## What is "in the DOM"? A menu Element node that is not part
985 ## of the Document tree is in the DOM? A menu Element node that
986 ## belong to another Document tree is in the DOM?
987 },
988 irrelevant => $GetHTMLBooleanAttrChecker->('irrelevant'),
989 tabindex => $HTMLIntegerAttrChecker,
990 };
991
992 for (qw/
993 onabort onbeforeunload onblur onchange onclick oncontextmenu
994 ondblclick ondrag ondragend ondragenter ondragleave ondragover
995 ondragstart ondrop onerror onfocus onkeydown onkeypress
996 onkeyup onload onmessage onmousedown onmousemove onmouseout
997 onmouseover onmouseup onmousewheel onresize onscroll onselect
998 onsubmit onunload
999 /) {
1000 $HTMLAttrChecker->{$_} = $HTMLEventHandlerAttrChecker;
1001 }
1002
1003 my $GetHTMLAttrsChecker = sub {
1004 my $element_specific_checker = shift;
1005 return sub {
1006 my ($self, $todo) = @_;
1007 for my $attr (@{$todo->{node}->attributes}) {
1008 my $attr_ns = $attr->namespace_uri;
1009 $attr_ns = '' unless defined $attr_ns;
1010 my $attr_ln = $attr->manakai_local_name;
1011 my $checker;
1012 if ($attr_ns eq '') {
1013 $checker = $element_specific_checker->{$attr_ln}
1014 || $HTMLAttrChecker->{$attr_ln};
1015 }
1016 $checker ||= $AttrChecker->{$attr_ns}->{$attr_ln}
1017 || $AttrChecker->{$attr_ns}->{''};
1018 if ($checker) {
1019 $checker->($self, $attr, $todo);
1020 } else {
1021 $self->{onerror}->(node => $attr, level => 'unsupported',
1022 type => 'attribute');
1023 ## ISSUE: No comformance createria for unknown attributes in the spec
1024 }
1025 }
1026 };
1027 }; # $GetHTMLAttrsChecker
1028
1029 $Element->{$HTML_NS}->{''} = {
1030 attrs_checker => $GetHTMLAttrsChecker->({}),
1031 checker => $ElementDefault->{checker},
1032 };
1033
1034 $Element->{$HTML_NS}->{html} = {
1035 is_root => 1,
1036 attrs_checker => $GetHTMLAttrsChecker->({
1037 xmlns => sub {
1038 my ($self, $attr) = @_;
1039 my $value = $attr->value;
1040 unless ($value eq $HTML_NS) {
1041 $self->{onerror}->(node => $attr, type => 'invalid attribute value');
1042 }
1043 unless ($attr->owner_document->manakai_is_html) {
1044 $self->{onerror}->(node => $attr, type => 'in XML:xmlns');
1045 ## TODO: Test
1046 }
1047 },
1048 }),
1049 checker => sub {
1050 my ($self, $todo) = @_;
1051 my $el = $todo->{node};
1052 my $new_todos = [];
1053 my @nodes = (@{$el->child_nodes});
1054
1055 my $phase = 'before head';
1056 while (@nodes) {
1057 my $node = shift @nodes;
1058 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
1059
1060 my $nt = $node->node_type;
1061 if ($nt == 1) {
1062 my $node_ns = $node->namespace_uri;
1063 $node_ns = '' unless defined $node_ns;
1064 my $node_ln = $node->manakai_local_name;
1065 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
1066 if ($phase eq 'before head') {
1067 if ($node_ns eq $HTML_NS and $node_ln eq 'head') {
1068 $phase = 'after head';
1069 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'body') {
1070 $self->{onerror}->(node => $node, type => 'ps element missing:head');
1071 $phase = 'after body';
1072 } else {
1073 $not_allowed = 1;
1074 # before head
1075 }
1076 } elsif ($phase eq 'after head') {
1077 if ($node_ns eq $HTML_NS and $node_ln eq 'body') {
1078 $phase = 'after body';
1079 } else {
1080 $not_allowed = 1;
1081 # after head
1082 }
1083 } else { #elsif ($phase eq 'after body') {
1084 $not_allowed = 1;
1085 # after body
1086 }
1087 $self->{onerror}->(node => $node, type => 'element not allowed')
1088 if $not_allowed;
1089 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
1090 unshift @nodes, @$sib;
1091 push @$new_todos, @$ch;
1092 } elsif ($nt == 3 or $nt == 4) {
1093 if ($node->data =~ /[^\x09-\x0D\x20]/) {
1094 $self->{onerror}->(node => $node, type => 'character not allowed');
1095 }
1096 } elsif ($nt == 5) {
1097 unshift @nodes, @{$node->child_nodes};
1098 }
1099 }
1100
1101 if ($phase eq 'before head') {
1102 $self->{onerror}->(node => $el, type => 'child element missing:head');
1103 $self->{onerror}->(node => $el, type => 'child element missing:body');
1104 } elsif ($phase eq 'after head') {
1105 $self->{onerror}->(node => $el, type => 'child element missing:body');
1106 }
1107
1108 return ($new_todos);
1109 },
1110 };
1111
1112 $Element->{$HTML_NS}->{head} = {
1113 attrs_checker => $GetHTMLAttrsChecker->({}),
1114 checker => sub {
1115 my ($self, $todo) = @_;
1116 my $el = $todo->{node};
1117 my $new_todos = [];
1118 my @nodes = (@{$el->child_nodes});
1119
1120 my $has_title;
1121 my $phase = 'initial'; # 'after charset', 'after base'
1122 while (@nodes) {
1123 my $node = shift @nodes;
1124 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
1125
1126 my $nt = $node->node_type;
1127 if ($nt == 1) {
1128 my $node_ns = $node->namespace_uri;
1129 $node_ns = '' unless defined $node_ns;
1130 my $node_ln = $node->manakai_local_name;
1131 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
1132 if ($node_ns eq $HTML_NS and $node_ln eq 'title') {
1133 $phase = 'after base';
1134 unless ($has_title) {
1135 $has_title = 1;
1136 } else {
1137 $not_allowed = 1;
1138 }
1139 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'meta') {
1140 if ($node->has_attribute_ns (undef, 'charset')) {
1141 if ($phase eq 'initial') {
1142 $phase = 'after charset';
1143 } else {
1144 $not_allowed = 1;
1145 ## NOTE: See also |base|'s "contexts" field in the spec
1146 }
1147 } else {
1148 $phase = 'after base';
1149 }
1150 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'base') {
1151 if ($phase eq 'initial' or $phase eq 'after charset') {
1152 $phase = 'after base';
1153 } else {
1154 $not_allowed = 1;
1155 }
1156 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'style') {
1157 $phase = 'after base';
1158 if ($node->has_attribute_ns (undef, 'scoped')) {
1159 $not_allowed = 1;
1160 }
1161 } elsif ($HTMLMetadataElements->{$node_ns}->{$node_ln}) {
1162 $phase = 'after base';
1163 } else {
1164 $not_allowed = 1;
1165 }
1166 $self->{onerror}->(node => $node, type => 'element not allowed')
1167 if $not_allowed;
1168 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
1169 unshift @nodes, @$sib;
1170 push @$new_todos, @$ch;
1171 } elsif ($nt == 3 or $nt == 4) {
1172 if ($node->data =~ /[^\x09-\x0D\x20]/) {
1173 $self->{onerror}->(node => $node, type => 'character not allowed');
1174 }
1175 } elsif ($nt == 5) {
1176 unshift @nodes, @{$node->child_nodes};
1177 }
1178 }
1179 unless ($has_title) {
1180 $self->{onerror}->(node => $el, type => 'child element missing:title');
1181 }
1182 return ($new_todos);
1183 },
1184 };
1185
1186 $Element->{$HTML_NS}->{title} = {
1187 attrs_checker => $GetHTMLAttrsChecker->({}),
1188 checker => $HTMLTextChecker,
1189 };
1190
1191 $Element->{$HTML_NS}->{base} = {
1192 attrs_checker => $GetHTMLAttrsChecker->({
1193 href => $HTMLURIAttrChecker,
1194 target => $HTMLTargetAttrChecker,
1195 }),
1196 checker => $HTMLEmptyChecker,
1197 };
1198
1199 $Element->{$HTML_NS}->{link} = {
1200 attrs_checker => sub {
1201 my ($self, $todo) = @_;
1202 $GetHTMLAttrsChecker->({
1203 href => $HTMLURIAttrChecker,
1204 rel => sub { $HTMLLinkTypesAttrChecker->(0, @_) },
1205 media => $HTMLMQAttrChecker,
1206 hreflang => $HTMLLanguageTagAttrChecker,
1207 type => $HTMLIMTAttrChecker,
1208 ## NOTE: Though |title| has special semantics,
1209 ## syntactically same as the |title| as global attribute.
1210 })->($self, $todo);
1211 unless ($todo->{node}->has_attribute_ns (undef, 'href')) {
1212 $self->{onerror}->(node => $todo->{node},
1213 type => 'attribute missing:href');
1214 }
1215 unless ($todo->{node}->has_attribute_ns (undef, 'rel')) {
1216 $self->{onerror}->(node => $todo->{node},
1217 type => 'attribute missing:rel');
1218 }
1219 },
1220 checker => $HTMLEmptyChecker,
1221 };
1222
1223 $Element->{$HTML_NS}->{meta} = {
1224 attrs_checker => sub {
1225 my ($self, $todo) = @_;
1226 my $name_attr;
1227 my $http_equiv_attr;
1228 my $charset_attr;
1229 my $content_attr;
1230 for my $attr (@{$todo->{node}->attributes}) {
1231 my $attr_ns = $attr->namespace_uri;
1232 $attr_ns = '' unless defined $attr_ns;
1233 my $attr_ln = $attr->manakai_local_name;
1234 my $checker;
1235 if ($attr_ns eq '') {
1236 if ($attr_ln eq 'content') {
1237 $content_attr = $attr;
1238 $checker = 1;
1239 } elsif ($attr_ln eq 'name') {
1240 $name_attr = $attr;
1241 $checker = 1;
1242 } elsif ($attr_ln eq 'http-equiv') {
1243 $http_equiv_attr = $attr;
1244 $checker = 1;
1245 } elsif ($attr_ln eq 'charset') {
1246 $charset_attr = $attr;
1247 $checker = 1;
1248 } else {
1249 $checker = $HTMLAttrChecker->{$attr_ln}
1250 || $AttrChecker->{$attr_ns}->{$attr_ln}
1251 || $AttrChecker->{$attr_ns}->{''};
1252 }
1253 } else {
1254 $checker ||= $AttrChecker->{$attr_ns}->{$attr_ln}
1255 || $AttrChecker->{$attr_ns}->{''};
1256 }
1257 if ($checker) {
1258 $checker->($self, $attr) if ref $checker;
1259 } else {
1260 $self->{onerror}->(node => $attr, level => 'unsupported',
1261 type => 'attribute');
1262 ## ISSUE: No comformance createria for unknown attributes in the spec
1263 }
1264 }
1265
1266 if (defined $name_attr) {
1267 if (defined $http_equiv_attr) {
1268 $self->{onerror}->(node => $http_equiv_attr,
1269 type => 'attribute not allowed');
1270 } elsif (defined $charset_attr) {
1271 $self->{onerror}->(node => $charset_attr,
1272 type => 'attribute not allowed');
1273 }
1274 my $metadata_name = $name_attr->value;
1275 my $metadata_value;
1276 if (defined $content_attr) {
1277 $metadata_value = $content_attr->value;
1278 } else {
1279 $self->{onerror}->(node => $todo->{node},
1280 type => 'attribute missing:content');
1281 $metadata_value = '';
1282 }
1283 } elsif (defined $http_equiv_attr) {
1284 if (defined $charset_attr) {
1285 $self->{onerror}->(node => $charset_attr,
1286 type => 'attribute not allowed');
1287 }
1288 unless (defined $content_attr) {
1289 $self->{onerror}->(node => $todo->{node},
1290 type => 'attribute missing:content');
1291 }
1292 } elsif (defined $charset_attr) {
1293 if (defined $content_attr) {
1294 $self->{onerror}->(node => $content_attr,
1295 type => 'attribute not allowed');
1296 }
1297 } else {
1298 if (defined $content_attr) {
1299 $self->{onerror}->(node => $content_attr,
1300 type => 'attribute not allowed');
1301 $self->{onerror}->(node => $todo->{node},
1302 type => 'attribute missing:name|http-equiv');
1303 } else {
1304 $self->{onerror}->(node => $todo->{node},
1305 type => 'attribute missing:name|http-equiv|charset');
1306 }
1307 }
1308
1309 ## TODO: metadata conformance
1310
1311 ## TODO: pragma conformance
1312 if (defined $http_equiv_attr) { ## An enumerated attribute
1313 my $keyword = lc $http_equiv_attr->value; ## TODO: ascii case?
1314 if ({
1315 'refresh' => 1,
1316 'default-style' => 1,
1317 }->{$keyword}) {
1318 #
1319 } else {
1320 $self->{onerror}->(node => $http_equiv_attr,
1321 type => 'enumerated:invalid');
1322 }
1323 }
1324
1325 if (defined $charset_attr) {
1326 unless ($todo->{node}->owner_document->manakai_is_html) {
1327 $self->{onerror}->(node => $charset_attr,
1328 type => 'in XML:charset');
1329 }
1330 ## TODO: charset
1331 }
1332 },
1333 checker => $HTMLEmptyChecker,
1334 };
1335
1336 $Element->{$HTML_NS}->{style} = {
1337 attrs_checker => $GetHTMLAttrsChecker->({
1338 type => $HTMLIMTAttrChecker, ## TODO: MUST be a styling language
1339 media => $HTMLMQAttrChecker,
1340 scoped => $GetHTMLBooleanAttrChecker->('scoped'),
1341 ## NOTE: |title| has special semantics for |style|s, but is syntactically
1342 ## not different
1343 }),
1344 checker => sub {
1345 ## NOTE: |html:style| has no conformance creteria on content model
1346 my ($self, $todo) = @_;
1347 my $type = $todo->{node}->get_attribute_ns (undef, 'type');
1348 $type = 'text/css' unless defined $type;
1349 $self->{onerror}->(node => $todo->{node}, level => 'unsupported',
1350 type => 'style:'.$type); ## TODO: $type normalization
1351 return $AnyChecker->($self, $todo);
1352 },
1353 };
1354
1355 $Element->{$HTML_NS}->{body} = {
1356 attrs_checker => $GetHTMLAttrsChecker->({}),
1357 checker => $HTMLBlockChecker,
1358 };
1359
1360 $Element->{$HTML_NS}->{section} = {
1361 attrs_checker => $GetHTMLAttrsChecker->({}),
1362 checker => $HTMLStylableBlockChecker,
1363 };
1364
1365 $Element->{$HTML_NS}->{nav} = {
1366 attrs_checker => $GetHTMLAttrsChecker->({}),
1367 checker => $HTMLBlockOrInlineChecker,
1368 };
1369
1370 $Element->{$HTML_NS}->{article} = {
1371 attrs_checker => $GetHTMLAttrsChecker->({}),
1372 checker => $HTMLStylableBlockChecker,
1373 };
1374
1375 $Element->{$HTML_NS}->{blockquote} = {
1376 attrs_checker => $GetHTMLAttrsChecker->({
1377 cite => $HTMLURIAttrChecker,
1378 }),
1379 checker => $HTMLBlockChecker,
1380 };
1381
1382 $Element->{$HTML_NS}->{aside} = {
1383 attrs_checker => $GetHTMLAttrsChecker->({}),
1384 checker => $GetHTMLZeroOrMoreThenBlockOrInlineChecker->($HTML_NS, 'style'),
1385 };
1386
1387 $Element->{$HTML_NS}->{h1} = {
1388 attrs_checker => $GetHTMLAttrsChecker->({}),
1389 checker => sub {
1390 my ($self, $todo) = @_;
1391 $todo->{flag}->{has_heading}->[0] = 1;
1392 return $HTMLSignificantStrictlyInlineChecker->($self, $todo);
1393 },
1394 };
1395
1396 $Element->{$HTML_NS}->{h2} = {
1397 attrs_checker => $GetHTMLAttrsChecker->({}),
1398 checker => $Element->{$HTML_NS}->{h1}->{checker},
1399 };
1400
1401 $Element->{$HTML_NS}->{h3} = {
1402 attrs_checker => $GetHTMLAttrsChecker->({}),
1403 checker => $Element->{$HTML_NS}->{h1}->{checker},
1404 };
1405
1406 $Element->{$HTML_NS}->{h4} = {
1407 attrs_checker => $GetHTMLAttrsChecker->({}),
1408 checker => $Element->{$HTML_NS}->{h1}->{checker},
1409 };
1410
1411 $Element->{$HTML_NS}->{h5} = {
1412 attrs_checker => $GetHTMLAttrsChecker->({}),
1413 checker => $Element->{$HTML_NS}->{h1}->{checker},
1414 };
1415
1416 $Element->{$HTML_NS}->{h6} = {
1417 attrs_checker => $GetHTMLAttrsChecker->({}),
1418 checker => $Element->{$HTML_NS}->{h1}->{checker},
1419 };
1420
1421 $Element->{$HTML_NS}->{header} = {
1422 attrs_checker => $GetHTMLAttrsChecker->({}),
1423 checker => sub {
1424 my ($self, $todo) = @_;
1425 my $old_flag = $todo->{flag}->{has_heading} || [];
1426 my $new_flag = [];
1427 local $todo->{flag}->{has_heading} = $new_flag;
1428 my $node = $todo->{node};
1429
1430 my $end = $self->_add_minuses
1431 ({$HTML_NS => {qw/header 1 footer 1/}},
1432 $HTMLSectioningElements);
1433 my ($new_todos, $ch) = $HTMLBlockChecker->($self, $todo);
1434 push @$new_todos, $end,
1435 {type => 'code', code => sub {
1436 if ($new_flag->[0]) {
1437 $old_flag->[0] = 1;
1438 } else {
1439 $self->{onerror}->(node => $node, type => 'element missing:hn');
1440 }
1441 }};
1442 return ($new_todos, $ch);
1443 },
1444 };
1445
1446 $Element->{$HTML_NS}->{footer} = {
1447 attrs_checker => $GetHTMLAttrsChecker->({}),
1448 checker => sub { ## block -hn -header -footer -sectioning or inline
1449 my ($self, $todo) = @_;
1450 my $el = $todo->{node};
1451 my $new_todos = [];
1452 my @nodes = (@{$el->child_nodes});
1453
1454 my $content = 'block-or-inline'; # or 'block' or 'inline'
1455 my @block_not_inline;
1456 while (@nodes) {
1457 my $node = shift @nodes;
1458 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
1459
1460 my $nt = $node->node_type;
1461 if ($nt == 1) {
1462 my $node_ns = $node->namespace_uri;
1463 $node_ns = '' unless defined $node_ns;
1464 my $node_ln = $node->manakai_local_name;
1465 my $not_allowed;
1466 if ($self->{minuses}->{$node_ns}->{$node_ln}) {
1467 $not_allowed = 1;
1468 } elsif ($node_ns eq $HTML_NS and
1469 {
1470 qw/h1 1 h2 1 h3 1 h4 1 h5 1 h6 1 header 1 footer 1/
1471 }->{$node_ln}) {
1472 $not_allowed = 1;
1473 } elsif ($HTMLSectioningElements->{$node_ns}->{$node_ln}) {
1474 $not_allowed = 1;
1475 }
1476 if ($content eq 'block') {
1477 $not_allowed = 1
1478 unless $HTMLBlockLevelElements->{$node_ns}->{$node_ln};
1479 } elsif ($content eq 'inline') {
1480 $not_allowed = 1
1481 unless $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln} or
1482 $HTMLStructuredInlineLevelElements->{$node_ns}->{$node_ln};
1483 } else {
1484 my $is_block = $HTMLBlockLevelElements->{$node_ns}->{$node_ln};
1485 my $is_inline
1486 = $HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln} ||
1487 $HTMLStructuredInlineLevelElements->{$node_ns}->{$node_ln};
1488
1489 push @block_not_inline, $node
1490 if $is_block and not $is_inline and not $not_allowed;
1491 unless ($is_block) {
1492 $content = 'inline';
1493 for (@block_not_inline) {
1494 $self->{onerror}->(node => $_, type => 'element not allowed');
1495 }
1496 $not_allowed = 1 unless $is_inline;
1497 }
1498 }
1499 $self->{onerror}->(node => $node, type => 'element not allowed')
1500 if $not_allowed;
1501 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
1502 unshift @nodes, @$sib;
1503 push @$new_todos, @$ch;
1504 } elsif ($nt == 3 or $nt == 4) {
1505 if ($node->data =~ /[^\x09-\x0D\x20]/) {
1506 if ($content eq 'block') {
1507 $self->{onerror}->(node => $node, type => 'character not allowed');
1508 } else {
1509 $content = 'inline';
1510 for (@block_not_inline) {
1511 $self->{onerror}->(node => $_, type => 'element not allowed');
1512 }
1513 }
1514 }
1515 } elsif ($nt == 5) {
1516 unshift @nodes, @{$node->child_nodes};
1517 }
1518 }
1519
1520 my $end = $self->_add_minuses
1521 ({$HTML_NS => {qw/h1 1 h2 1 h3 1 h4 1 h5 1 h6 1/}},
1522 $HTMLSectioningElements);
1523 push @$new_todos, $end;
1524
1525 if ($content eq 'inline') {
1526 for (@$new_todos) {
1527 $_->{inline} = 1;
1528 }
1529 }
1530
1531 return ($new_todos);
1532 },
1533 };
1534
1535 $Element->{$HTML_NS}->{address} = {
1536 attrs_checker => $GetHTMLAttrsChecker->({}),
1537 checker => $HTMLInlineChecker,
1538 };
1539
1540 $Element->{$HTML_NS}->{p} = {
1541 attrs_checker => $GetHTMLAttrsChecker->({}),
1542 checker => $HTMLSignificantInlineChecker,
1543 };
1544
1545 $Element->{$HTML_NS}->{hr} = {
1546 attrs_checker => $GetHTMLAttrsChecker->({}),
1547 checker => $HTMLEmptyChecker,
1548 };
1549
1550 $Element->{$HTML_NS}->{br} = {
1551 attrs_checker => $GetHTMLAttrsChecker->({}),
1552 checker => $HTMLEmptyChecker,
1553 };
1554
1555 $Element->{$HTML_NS}->{dialog} = {
1556 attrs_checker => $GetHTMLAttrsChecker->({}),
1557 checker => sub {
1558 my ($self, $todo) = @_;
1559 my $el = $todo->{node};
1560 my $new_todos = [];
1561 my @nodes = (@{$el->child_nodes});
1562
1563 my $phase = 'before dt';
1564 while (@nodes) {
1565 my $node = shift @nodes;
1566 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
1567
1568 my $nt = $node->node_type;
1569 if ($nt == 1) {
1570 my $node_ns = $node->namespace_uri;
1571 $node_ns = '' unless defined $node_ns;
1572 my $node_ln = $node->manakai_local_name;
1573 ## NOTE: |minuses| list is not checked since redundant
1574 if ($phase eq 'before dt') {
1575 if ($node_ns eq $HTML_NS and $node_ln eq 'dt') {
1576 $phase = 'before dd';
1577 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'dd') {
1578 $self->{onerror}
1579 ->(node => $node, type => 'ps element missing:dt');
1580 $phase = 'before dt';
1581 } else {
1582 $self->{onerror}->(node => $node, type => 'element not allowed');
1583 }
1584 } else { # before dd
1585 if ($node_ns eq $HTML_NS and $node_ln eq 'dd') {
1586 $phase = 'before dt';
1587 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'dt') {
1588 $self->{onerror}
1589 ->(node => $node, type => 'ps element missing:dd');
1590 $phase = 'before dd';
1591 } else {
1592 $self->{onerror}->(node => $node, type => 'element not allowed');
1593 }
1594 }
1595 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
1596 unshift @nodes, @$sib;
1597 push @$new_todos, @$ch;
1598 } elsif ($nt == 3 or $nt == 4) {
1599 if ($node->data =~ /[^\x09-\x0D\x20]/) {
1600 $self->{onerror}->(node => $node, type => 'character not allowed');
1601 }
1602 } elsif ($nt == 5) {
1603 unshift @nodes, @{$node->child_nodes};
1604 }
1605 }
1606 if ($phase eq 'before dd') {
1607 $self->{onerror}->(node => $el, type => 'ps element missing:dd');
1608 }
1609 return ($new_todos);
1610 },
1611 };
1612
1613 $Element->{$HTML_NS}->{pre} = {
1614 attrs_checker => $GetHTMLAttrsChecker->({}),
1615 checker => $HTMLStrictlyInlineChecker,
1616 };
1617
1618 $Element->{$HTML_NS}->{ol} = {
1619 attrs_checker => $GetHTMLAttrsChecker->({
1620 start => $HTMLIntegerAttrChecker,
1621 }),
1622 checker => sub {
1623 my ($self, $todo) = @_;
1624 my $el = $todo->{node};
1625 my $new_todos = [];
1626 my @nodes = (@{$el->child_nodes});
1627
1628 while (@nodes) {
1629 my $node = shift @nodes;
1630 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
1631
1632 my $nt = $node->node_type;
1633 if ($nt == 1) {
1634 my $node_ns = $node->namespace_uri;
1635 $node_ns = '' unless defined $node_ns;
1636 my $node_ln = $node->manakai_local_name;
1637 ## NOTE: |minuses| list is not checked since redundant
1638 unless ($node_ns eq $HTML_NS and $node_ln eq 'li') {
1639 $self->{onerror}->(node => $node, type => 'element not allowed');
1640 }
1641 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
1642 unshift @nodes, @$sib;
1643 push @$new_todos, @$ch;
1644 } elsif ($nt == 3 or $nt == 4) {
1645 if ($node->data =~ /[^\x09-\x0D\x20]/) {
1646 $self->{onerror}->(node => $node, type => 'character not allowed');
1647 }
1648 } elsif ($nt == 5) {
1649 unshift @nodes, @{$node->child_nodes};
1650 }
1651 }
1652
1653 if ($todo->{inline}) {
1654 for (@$new_todos) {
1655 $_->{inline} = 1;
1656 }
1657 }
1658 return ($new_todos);
1659 },
1660 };
1661
1662 $Element->{$HTML_NS}->{ul} = {
1663 attrs_checker => $GetHTMLAttrsChecker->({}),
1664 checker => $Element->{$HTML_NS}->{ol}->{checker},
1665 };
1666
1667
1668 $Element->{$HTML_NS}->{li} = {
1669 attrs_checker => $GetHTMLAttrsChecker->({
1670 start => sub {
1671 my ($self, $attr) = @_;
1672 my $parent = $attr->owner_element->manakai_parent_element;
1673 if (defined $parent) {
1674 my $parent_ns = $parent->namespace_uri;
1675 $parent_ns = '' unless defined $parent_ns;
1676 my $parent_ln = $parent->manakai_local_name;
1677 unless ($parent_ns eq $HTML_NS and $parent_ln eq 'ol') {
1678 $self->{onerror}->(node => $attr, level => 'unsupported',
1679 type => 'attribute');
1680 }
1681 }
1682 $HTMLIntegerAttrChecker->($self, $attr);
1683 },
1684 }),
1685 checker => sub {
1686 my ($self, $todo) = @_;
1687 if ($todo->{inline}) {
1688 return $HTMLInlineChecker->($self, $todo);
1689 } else {
1690 return $HTMLBlockOrInlineChecker->($self, $todo);
1691 }
1692 },
1693 };
1694
1695 $Element->{$HTML_NS}->{dl} = {
1696 attrs_checker => $GetHTMLAttrsChecker->({}),
1697 checker => sub {
1698 my ($self, $todo) = @_;
1699 my $el = $todo->{node};
1700 my $new_todos = [];
1701 my @nodes = (@{$el->child_nodes});
1702
1703 my $phase = 'before dt';
1704 while (@nodes) {
1705 my $node = shift @nodes;
1706 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
1707
1708 my $nt = $node->node_type;
1709 if ($nt == 1) {
1710 my $node_ns = $node->namespace_uri;
1711 $node_ns = '' unless defined $node_ns;
1712 my $node_ln = $node->manakai_local_name;
1713 ## NOTE: |minuses| list is not checked since redundant
1714 if ($phase eq 'in dds') {
1715 if ($node_ns eq $HTML_NS and $node_ln eq 'dd') {
1716 #$phase = 'in dds';
1717 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'dt') {
1718 $phase = 'in dts';
1719 } else {
1720 $self->{onerror}->(node => $node, type => 'element not allowed');
1721 }
1722 } elsif ($phase eq 'in dts') {
1723 if ($node_ns eq $HTML_NS and $node_ln eq 'dt') {
1724 #$phase = 'in dts';
1725 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'dd') {
1726 $phase = 'in dds';
1727 } else {
1728 $self->{onerror}->(node => $node, type => 'element not allowed');
1729 }
1730 } else { # before dt
1731 if ($node_ns eq $HTML_NS and $node_ln eq 'dt') {
1732 $phase = 'in dts';
1733 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'dd') {
1734 $self->{onerror}
1735 ->(node => $node, type => 'ps element missing:dt');
1736 $phase = 'in dds';
1737 } else {
1738 $self->{onerror}->(node => $node, type => 'element not allowed');
1739 }
1740 }
1741 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
1742 unshift @nodes, @$sib;
1743 push @$new_todos, @$ch;
1744 } elsif ($nt == 3 or $nt == 4) {
1745 if ($node->data =~ /[^\x09-\x0D\x20]/) {
1746 $self->{onerror}->(node => $node, type => 'character not allowed');
1747 }
1748 } elsif ($nt == 5) {
1749 unshift @nodes, @{$node->child_nodes};
1750 }
1751 }
1752 if ($phase eq 'in dts') {
1753 $self->{onerror}->(node => $el, type => 'ps element missing:dd');
1754 }
1755
1756 if ($todo->{inline}) {
1757 for (@$new_todos) {
1758 $_->{inline} = 1;
1759 }
1760 }
1761 return ($new_todos);
1762 },
1763 };
1764
1765 $Element->{$HTML_NS}->{dt} = {
1766 attrs_checker => $GetHTMLAttrsChecker->({}),
1767 checker => $HTMLStrictlyInlineChecker,
1768 };
1769
1770 $Element->{$HTML_NS}->{dd} = {
1771 attrs_checker => $GetHTMLAttrsChecker->({}),
1772 checker => $Element->{$HTML_NS}->{li}->{checker},
1773 };
1774
1775 $Element->{$HTML_NS}->{a} = {
1776 attrs_checker => sub {
1777 my ($self, $todo) = @_;
1778 my %attr;
1779 for my $attr (@{$todo->{node}->attributes}) {
1780 my $attr_ns = $attr->namespace_uri;
1781 $attr_ns = '' unless defined $attr_ns;
1782 my $attr_ln = $attr->manakai_local_name;
1783 my $checker;
1784 if ($attr_ns eq '') {
1785 $checker = {
1786 target => $HTMLTargetAttrChecker,
1787 href => $HTMLURIAttrChecker,
1788 ping => $HTMLSpaceURIsAttrChecker,
1789 rel => sub { $HTMLLinkTypesAttrChecker->(1, @_) },
1790 media => $HTMLMQAttrChecker,
1791 hreflang => $HTMLLanguageTagAttrChecker,
1792 type => $HTMLIMTAttrChecker,
1793 }->{$attr_ln};
1794 if ($checker) {
1795 $attr{$attr_ln} = $attr;
1796 } else {
1797 $checker = $HTMLAttrChecker->{$attr_ln};
1798 }
1799 }
1800 $checker ||= $AttrChecker->{$attr_ns}->{$attr_ln}
1801 || $AttrChecker->{$attr_ns}->{''};
1802 if ($checker) {
1803 $checker->($self, $attr) if ref $checker;
1804 } else {
1805 $self->{onerror}->(node => $attr, level => 'unsupported',
1806 type => 'attribute');
1807 ## ISSUE: No comformance createria for unknown attributes in the spec
1808 }
1809 }
1810
1811 unless (defined $attr{href}) {
1812 for (qw/target ping rel media hreflang type/) {
1813 if (defined $attr{$_}) {
1814 $self->{onerror}->(node => $attr{$_},
1815 type => 'attribute not allowed');
1816 }
1817 }
1818 }
1819 },
1820 checker => sub {
1821 my ($self, $todo) = @_;
1822
1823 my $end = $self->_add_minuses ($HTMLInteractiveElements);
1824 my ($new_todos, $ch)
1825 = $HTMLSignificantInlineOrStrictlyInlineChecker->($self, $todo);
1826 push @$new_todos, $end;
1827
1828 $_->{flag}->{has_a} = 1 for @$new_todos;
1829
1830 return ($new_todos, $ch);
1831 },
1832 };
1833
1834 $Element->{$HTML_NS}->{q} = {
1835 attrs_checker => $GetHTMLAttrsChecker->({
1836 cite => $HTMLURIAttrChecker,
1837 }),
1838 checker => $HTMLInlineOrStrictlyInlineChecker,
1839 };
1840
1841 $Element->{$HTML_NS}->{cite} = {
1842 attrs_checker => $GetHTMLAttrsChecker->({}),
1843 checker => $HTMLStrictlyInlineChecker,
1844 };
1845
1846 $Element->{$HTML_NS}->{em} = {
1847 attrs_checker => $GetHTMLAttrsChecker->({}),
1848 checker => $HTMLInlineOrStrictlyInlineChecker,
1849 };
1850
1851 $Element->{$HTML_NS}->{strong} = {
1852 attrs_checker => $GetHTMLAttrsChecker->({}),
1853 checker => $HTMLInlineOrStrictlyInlineChecker,
1854 };
1855
1856 $Element->{$HTML_NS}->{small} = {
1857 attrs_checker => $GetHTMLAttrsChecker->({}),
1858 checker => $HTMLInlineOrStrictlyInlineChecker,
1859 };
1860
1861 $Element->{$HTML_NS}->{m} = {
1862 attrs_checker => $GetHTMLAttrsChecker->({}),
1863 checker => $HTMLInlineOrStrictlyInlineChecker,
1864 };
1865
1866 $Element->{$HTML_NS}->{dfn} = {
1867 attrs_checker => $GetHTMLAttrsChecker->({}),
1868 checker => sub {
1869 my ($self, $todo) = @_;
1870
1871 my $end = $self->_add_minuses ({$HTML_NS => {dfn => 1}});
1872 my ($sib, $ch) = $HTMLStrictlyInlineChecker->($self, $todo);
1873 push @$sib, $end;
1874
1875 my $node = $todo->{node};
1876 my $term = $node->get_attribute_ns (undef, 'title');
1877 unless (defined $term) {
1878 for my $child (@{$node->child_nodes}) {
1879 if ($child->node_type == 1) { # ELEMENT_NODE
1880 if (defined $term) {
1881 undef $term;
1882 last;
1883 } elsif ($child->manakai_local_name eq 'abbr') {
1884 my $nsuri = $child->namespace_uri;
1885 if (defined $nsuri and $nsuri eq $HTML_NS) {
1886 my $attr = $child->get_attribute_node_ns (undef, 'title');
1887 if ($attr) {
1888 $term = $attr->value;
1889 }
1890 }
1891 }
1892 } elsif ($child->node_type == 3 or $child->node_type == 4) {
1893 ## TEXT_NODE or CDATA_SECTION_NODE
1894 if ($child->data =~ /\A[\x09-\x0D\x20]+\z/) { # Inter-element whitespace
1895 next;
1896 }
1897 undef $term;
1898 last;
1899 }
1900 }
1901 unless (defined $term) {
1902 $term = $node->text_content;
1903 }
1904 }
1905 if ($self->{term}->{$term}) {
1906 $self->{onerror}->(node => $node, type => 'duplicate term');
1907 push @{$self->{term}->{$term}}, $node;
1908 } else {
1909 $self->{term}->{$term} = [$node];
1910 }
1911 ## ISSUE: The HTML5 algorithm does not work with |ruby| unless |dfn|
1912 ## has |title|.
1913
1914 return ($sib, $ch);
1915 },
1916 };
1917
1918 $Element->{$HTML_NS}->{abbr} = {
1919 attrs_checker => $GetHTMLAttrsChecker->({
1920 ## NOTE: |title| has special semantics for |abbr|s, but is syntactically
1921 ## not different. The spec says that the |title| MAY be omitted
1922 ## if there is a |dfn| whose defining term is the abbreviation,
1923 ## but it does not prohibit |abbr| w/o |title| in other cases.
1924 }),
1925 checker => $HTMLStrictlyInlineChecker,
1926 };
1927
1928 $Element->{$HTML_NS}->{time} = {
1929 attrs_checker => $GetHTMLAttrsChecker->({
1930 datetime => sub { 1 }, # checked in |checker|
1931 }),
1932 ## TODO: Write tests
1933 checker => sub {
1934 my ($self, $todo) = @_;
1935
1936 my $attr = $todo->{node}->get_attribute_node_ns (undef, 'datetime');
1937 my $input;
1938 my $reg_sp;
1939 my $input_node;
1940 if ($attr) {
1941 $input = $attr->value;
1942 $reg_sp = qr/[\x09-\x0D\x20]*/;
1943 $input_node = $attr;
1944 } else {
1945 $input = $todo->{node}->text_content;
1946 $reg_sp = qr/\p{Zs}*/;
1947 $input_node = $todo->{node};
1948
1949 ## ISSUE: What is the definition for "successfully extracts a date
1950 ## or time"? If the algorithm says the string is invalid but
1951 ## return some date or time, is it "successfully"?
1952 }
1953
1954 my $hour;
1955 my $minute;
1956 my $second;
1957 if ($input =~ /
1958 \A
1959 [\x09-\x0D\x20]*
1960 ([0-9]+) # 1
1961 (?>
1962 -([0-9]+) # 2
1963 -([0-9]+) # 3
1964 [\x09-\x0D\x20]*
1965 (?>
1966 T
1967 [\x09-\x0D\x20]*
1968 )?
1969 ([0-9]+) # 4
1970 :([0-9]+) # 5
1971 (?>
1972 :([0-9]+(?>\.[0-9]*)?|\.[0-9]*) # 6
1973 )?
1974 [\x09-\x0D\x20]*
1975 (?>
1976 Z
1977 [\x09-\x0D\x20]*
1978 |
1979 [+-]([0-9]+):([0-9]+) # 7, 8
1980 [\x09-\x0D\x20]*
1981 )?
1982 \z
1983 |
1984 :([0-9]+) # 9
1985 (?>
1986 :([0-9]+(?>\.[0-9]*)?|\.[0-9]*) # 10
1987 )?
1988 [\x09-\x0D\x20]*\z
1989 )
1990 /x) {
1991 if (defined $2) { ## YYYY-MM-DD T? hh:mm
1992 if (length $1 != 4 or length $2 != 2 or length $3 != 2 or
1993 length $4 != 2 or length $5 != 2) {
1994 $self->{onerror}->(node => $input_node,
1995 type => 'dateortime:syntax error');
1996 }
1997
1998 if (1 <= $2 and $2 <= 12) {
1999 $self->{onerror}->(node => $input_node, type => 'datetime:bad day')
2000 if $3 < 1 or
2001 $3 > [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]->[$2];
2002 $self->{onerror}->(node => $input_node, type => 'datetime:bad day')
2003 if $2 == 2 and $3 == 29 and
2004 not ($1 % 400 == 0 or ($1 % 4 == 0 and $1 % 100 != 0));
2005 } else {
2006 $self->{onerror}->(node => $input_node,
2007 type => 'datetime:bad month');
2008 }
2009
2010 ($hour, $minute, $second) = ($4, $5, $6);
2011
2012 if (defined $7) { ## [+-]hh:mm
2013 if (length $7 != 2 or length $8 != 2) {
2014 $self->{onerror}->(node => $input_node,
2015 type => 'dateortime:syntax error');
2016 }
2017
2018 $self->{onerror}->(node => $input_node,
2019 type => 'datetime:bad timezone hour')
2020 if $7 > 23;
2021 $self->{onerror}->(node => $input_node,
2022 type => 'datetime:bad timezone minute')
2023 if $8 > 59;
2024 }
2025 } else { ## hh:mm
2026 if (length $1 != 2 or length $9 != 2) {
2027 $self->{onerror}->(node => $input_node,
2028 type => qq'dateortime:syntax error');
2029 }
2030
2031 ($hour, $minute, $second) = ($1, $9, $10);
2032 }
2033
2034 $self->{onerror}->(node => $input_node, type => 'datetime:bad hour')
2035 if $hour > 23;
2036 $self->{onerror}->(node => $input_node, type => 'datetime:bad minute')
2037 if $minute > 59;
2038
2039 if (defined $second) { ## s
2040 ## NOTE: Integer part of second don't have to have length of two.
2041
2042 if (substr ($second, 0, 1) eq '.') {
2043 $self->{onerror}->(node => $input_node,
2044 type => 'dateortime:syntax error');
2045 }
2046
2047 $self->{onerror}->(node => $input_node, type => 'datetime:bad second')
2048 if $second >= 60;
2049 }
2050 } else {
2051 $self->{onerror}->(node => $input_node,
2052 type => 'dateortime:syntax error');
2053 }
2054
2055 return $HTMLStrictlyInlineChecker->($self, $todo);
2056 },
2057 };
2058
2059 $Element->{$HTML_NS}->{meter} = { ## TODO: "The recommended way of giving the value is to include it as contents of the element"
2060 attrs_checker => $GetHTMLAttrsChecker->({
2061 value => $GetHTMLFloatingPointNumberAttrChecker->(sub { 1 }),
2062 min => $GetHTMLFloatingPointNumberAttrChecker->(sub { 1 }),
2063 low => $GetHTMLFloatingPointNumberAttrChecker->(sub { 1 }),
2064 high => $GetHTMLFloatingPointNumberAttrChecker->(sub { 1 }),
2065 max => $GetHTMLFloatingPointNumberAttrChecker->(sub { 1 }),
2066 optimum => $GetHTMLFloatingPointNumberAttrChecker->(sub { 1 }),
2067 }),
2068 checker => $HTMLStrictlyInlineChecker,
2069 };
2070
2071 $Element->{$HTML_NS}->{progress} = { ## TODO: recommended to use content
2072 attrs_checker => $GetHTMLAttrsChecker->({
2073 value => $GetHTMLFloatingPointNumberAttrChecker->(sub { shift >= 0 }),
2074 max => $GetHTMLFloatingPointNumberAttrChecker->(sub { shift > 0 }),
2075 }),
2076 checker => $HTMLStrictlyInlineChecker,
2077 };
2078
2079 $Element->{$HTML_NS}->{code} = {
2080 attrs_checker => $GetHTMLAttrsChecker->({}),
2081 ## NOTE: Though |title| has special semantics,
2082 ## syntatically same as the |title| as global attribute.
2083 checker => $HTMLInlineOrStrictlyInlineChecker,
2084 };
2085
2086 $Element->{$HTML_NS}->{var} = {
2087 attrs_checker => $GetHTMLAttrsChecker->({}),
2088 ## NOTE: Though |title| has special semantics,
2089 ## syntatically same as the |title| as global attribute.
2090 checker => $HTMLStrictlyInlineChecker,
2091 };
2092
2093 $Element->{$HTML_NS}->{samp} = {
2094 attrs_checker => $GetHTMLAttrsChecker->({}),
2095 ## NOTE: Though |title| has special semantics,
2096 ## syntatically same as the |title| as global attribute.
2097 checker => $HTMLInlineOrStrictlyInlineChecker,
2098 };
2099
2100 $Element->{$HTML_NS}->{kbd} = {
2101 attrs_checker => $GetHTMLAttrsChecker->({}),
2102 checker => $HTMLStrictlyInlineChecker,
2103 };
2104
2105 $Element->{$HTML_NS}->{sub} = {
2106 attrs_checker => $GetHTMLAttrsChecker->({}),
2107 checker => $HTMLStrictlyInlineChecker,
2108 };
2109
2110 $Element->{$HTML_NS}->{sup} = {
2111 attrs_checker => $GetHTMLAttrsChecker->({}),
2112 checker => $HTMLStrictlyInlineChecker,
2113 };
2114
2115 $Element->{$HTML_NS}->{span} = {
2116 attrs_checker => $GetHTMLAttrsChecker->({}),
2117 ## NOTE: Though |title| has special semantics,
2118 ## syntatically same as the |title| as global attribute.
2119 checker => $HTMLInlineOrStrictlyInlineChecker,
2120 };
2121
2122 $Element->{$HTML_NS}->{i} = {
2123 attrs_checker => $GetHTMLAttrsChecker->({}),
2124 ## NOTE: Though |title| has special semantics,
2125 ## syntatically same as the |title| as global attribute.
2126 checker => $HTMLStrictlyInlineChecker,
2127 };
2128
2129 $Element->{$HTML_NS}->{b} = {
2130 attrs_checker => $GetHTMLAttrsChecker->({}),
2131 checker => $HTMLStrictlyInlineChecker,
2132 };
2133
2134 $Element->{$HTML_NS}->{bdo} = {
2135 attrs_checker => sub {
2136 my ($self, $todo) = @_;
2137 $GetHTMLAttrsChecker->({})->($self, $todo);
2138 unless ($todo->{node}->has_attribute_ns (undef, 'dir')) {
2139 $self->{onerror}->(node => $todo->{node}, type => 'attribute missing:dir');
2140 }
2141 },
2142 ## ISSUE: The spec does not directly say that |dir| is a enumerated attr.
2143 checker => $HTMLStrictlyInlineChecker,
2144 };
2145
2146 $Element->{$HTML_NS}->{ins} = {
2147 attrs_checker => $GetHTMLAttrsChecker->({
2148 cite => $HTMLURIAttrChecker,
2149 datetime => $HTMLDatetimeAttrChecker,
2150 }),
2151 checker => $HTMLTransparentChecker,
2152 };
2153
2154 $Element->{$HTML_NS}->{del} = {
2155 attrs_checker => $GetHTMLAttrsChecker->({
2156 cite => $HTMLURIAttrChecker,
2157 datetime => $HTMLDatetimeAttrChecker,
2158 }),
2159 checker => sub {
2160 my ($self, $todo) = @_;
2161
2162 my $parent = $todo->{node}->manakai_parent_element;
2163 if (defined $parent) {
2164 my $nsuri = $parent->namespace_uri;
2165 $nsuri = '' unless defined $nsuri;
2166 my $ln = $parent->manakai_local_name;
2167 my $eldef = $Element->{$nsuri}->{$ln} ||
2168 $Element->{$nsuri}->{''} ||
2169 $ElementDefault;
2170 return $eldef->{checker}->($self, $todo);
2171 } else {
2172 return $HTMLBlockOrInlineChecker->($self, $todo);
2173 }
2174 },
2175 };
2176
2177 ## TODO: figure
2178
2179 $Element->{$HTML_NS}->{img} = {
2180 attrs_checker => sub {
2181 my ($self, $todo) = @_;
2182 $GetHTMLAttrsChecker->({
2183 alt => sub { }, ## NOTE: No syntactical requirement
2184 src => $HTMLURIAttrChecker,
2185 usemap => $HTMLUsemapAttrChecker,
2186 ismap => sub {
2187 my ($self, $attr, $parent_todo) = @_;
2188 if (not $todo->{flag}->{has_a}) {
2189 $self->{onerror}->(node => $attr, type => 'attribute not allowed');
2190 }
2191 $GetHTMLBooleanAttrChecker->('ismap')->($self, $attr, $parent_todo);
2192 },
2193 ## TODO: height
2194 ## TODO: width
2195 })->($self, $todo);
2196 unless ($todo->{node}->has_attribute_ns (undef, 'alt')) {
2197 $self->{onerror}->(node => $todo->{node}, type => 'attribute missing:alt');
2198 }
2199 unless ($todo->{node}->has_attribute_ns (undef, 'src')) {
2200 $self->{onerror}->(node => $todo->{node}, type => 'attribute missing:src');
2201 }
2202 },
2203 checker => $HTMLEmptyChecker,
2204 };
2205
2206 $Element->{$HTML_NS}->{iframe} = {
2207 attrs_checker => $GetHTMLAttrsChecker->({
2208 src => $HTMLURIAttrChecker,
2209 }),
2210 checker => $HTMLTextChecker,
2211 };
2212
2213 $Element->{$HTML_NS}->{embed} = {
2214 attrs_checker => sub {
2215 my ($self, $todo) = @_;
2216 my $has_src;
2217 for my $attr (@{$todo->{node}->attributes}) {
2218 my $attr_ns = $attr->namespace_uri;
2219 $attr_ns = '' unless defined $attr_ns;
2220 my $attr_ln = $attr->manakai_local_name;
2221 my $checker;
2222 if ($attr_ns eq '') {
2223 if ($attr_ln eq 'src') {
2224 $checker = $HTMLURIAttrChecker;
2225 $has_src = 1;
2226 } elsif ($attr_ln eq 'type') {
2227 $checker = $HTMLIMTAttrChecker;
2228 } else {
2229 ## TODO: height
2230 ## TODO: width
2231 $checker = $HTMLAttrChecker->{$attr_ln}
2232 || sub { }; ## NOTE: Any local attribute is ok.
2233 }
2234 }
2235 $checker ||= $AttrChecker->{$attr_ns}->{$attr_ln}
2236 || $AttrChecker->{$attr_ns}->{''};
2237 if ($checker) {
2238 $checker->($self, $attr);
2239 } else {
2240 $self->{onerror}->(node => $attr, level => 'unsupported',
2241 type => 'attribute');
2242 ## ISSUE: No comformance createria for global attributes in the spec
2243 }
2244 }
2245
2246 unless ($has_src) {
2247 $self->{onerror}->(node => $todo->{node},
2248 type => 'attribute missing:src');
2249 }
2250 },
2251 checker => $HTMLEmptyChecker,
2252 };
2253
2254 $Element->{$HTML_NS}->{object} = {
2255 attrs_checker => sub {
2256 my ($self, $todo) = @_;
2257 $GetHTMLAttrsChecker->({
2258 data => $HTMLURIAttrChecker,
2259 type => $HTMLIMTAttrChecker,
2260 usemap => $HTMLUsemapAttrChecker,
2261 ## TODO: width
2262 ## TODO: height
2263 })->($self, $todo);
2264 unless ($todo->{node}->has_attribute_ns (undef, 'data')) {
2265 unless ($todo->{node}->has_attribute_ns (undef, 'type')) {
2266 $self->{onerror}->(node => $todo->{node},
2267 type => 'attribute missing:data|type');
2268 }
2269 }
2270 },
2271 checker => $ElementDefault->{checker}, ## TODO
2272 };
2273
2274 $Element->{$HTML_NS}->{param} = {
2275 attrs_checker => sub {
2276 my ($self, $todo) = @_;
2277 $GetHTMLAttrsChecker->({
2278 name => sub { },
2279 value => sub { },
2280 })->($self, $todo);
2281 unless ($todo->{node}->has_attribute_ns (undef, 'name')) {
2282 $self->{onerror}->(node => $todo->{node},
2283 type => 'attribute missing:name');
2284 }
2285 unless ($todo->{node}->has_attribute_ns (undef, 'value')) {
2286 $self->{onerror}->(node => $todo->{node},
2287 type => 'attribute missing:value');
2288 }
2289 },
2290 checker => $HTMLEmptyChecker,
2291 };
2292
2293 $Element->{$HTML_NS}->{video} = {
2294 attrs_checker => $GetHTMLAttrsChecker->({
2295 src => $HTMLURIAttrChecker,
2296 ## TODO: start, loopstart, loopend, end
2297 ## ISSUE: they MUST be "value time offset"s. Value?
2298 ## ISSUE: loopcount has no conformance creteria
2299 autoplay => $GetHTMLBooleanAttrChecker->('autoplay'),
2300 controls => $GetHTMLBooleanAttrChecker->('controls'),
2301 }),
2302 checker => sub {
2303 my ($self, $todo) = @_;
2304
2305 if ($todo->{node}->has_attribute_ns (undef, 'src')) {
2306 return $HTMLBlockOrInlineChecker->($self, $todo);
2307 } else {
2308 return $GetHTMLZeroOrMoreThenBlockOrInlineChecker->($HTML_NS, 'source')
2309 ->($self, $todo);
2310 }
2311 },
2312 };
2313
2314 $Element->{$HTML_NS}->{audio} = {
2315 attrs_checker => $Element->{$HTML_NS}->{video}->{attrs_checker},
2316 checker => $Element->{$HTML_NS}->{video}->{checker},
2317 };
2318
2319 $Element->{$HTML_NS}->{source} = {
2320 attrs_checker => sub {
2321 my ($self, $todo) = @_;
2322 $GetHTMLAttrsChecker->({
2323 src => $HTMLURIAttrChecker,
2324 type => $HTMLIMTAttrChecker,
2325 media => $HTMLMQAttrChecker,
2326 })->($self, $todo);
2327 unless ($todo->{node}->has_attribute_ns (undef, 'src')) {
2328 $self->{onerror}->(node => $todo->{node},
2329 type => 'attribute missing:src');
2330 }
2331 },
2332 checker => $HTMLEmptyChecker,
2333 };
2334
2335 $Element->{$HTML_NS}->{canvas} = {
2336 attrs_checker => $GetHTMLAttrsChecker->({
2337 height => $GetHTMLNonNegativeIntegerAttrChecker->(sub { 1 }),
2338 width => $GetHTMLNonNegativeIntegerAttrChecker->(sub { 1 }),
2339 }),
2340 checker => $HTMLInlineChecker,
2341 };
2342
2343 $Element->{$HTML_NS}->{map} = {
2344 attrs_checker => $GetHTMLAttrsChecker->({
2345 id => sub {
2346 ## NOTE: same as global |id=""|, with |$self->{map}| registeration
2347 my ($self, $attr) = @_;
2348 my $value = $attr->value;
2349 if (length $value > 0) {
2350 if ($self->{id}->{$value}) {
2351 $self->{onerror}->(node => $attr, type => 'duplicate ID');
2352 push @{$self->{id}->{$value}}, $attr;
2353 } else {
2354 $self->{id}->{$value} = [$attr];
2355 }
2356 } else {
2357 ## NOTE: MUST contain at least one character
2358 $self->{onerror}->(node => $attr, type => 'empty attribute value');
2359 }
2360 if ($value =~ /[\x09-\x0D\x20]/) {
2361 $self->{onerror}->(node => $attr, type => 'space in ID');
2362 }
2363 $self->{map}->{$value} ||= $attr;
2364 },
2365 }),
2366 checker => $HTMLBlockChecker,
2367 };
2368
2369 $Element->{$HTML_NS}->{area} = {
2370 attrs_checker => sub {
2371 my ($self, $todo) = @_;
2372 my %attr;
2373 my $coords;
2374 for my $attr (@{$todo->{node}->attributes}) {
2375 my $attr_ns = $attr->namespace_uri;
2376 $attr_ns = '' unless defined $attr_ns;
2377 my $attr_ln = $attr->manakai_local_name;
2378 my $checker;
2379 if ($attr_ns eq '') {
2380 $checker = {
2381 alt => sub { },
2382 ## NOTE: |alt| value has no conformance creteria.
2383 shape => $GetHTMLEnumeratedAttrChecker->({
2384 circ => -1, circle => 1,
2385 default => 1,
2386 poly => 1, polygon => -1,
2387 rect => 1, rectangle => -1,
2388 }),
2389 coords => sub {
2390 my ($self, $attr) = @_;
2391 my $value = $attr->value;
2392 if ($value =~ /\A-?[0-9]+(?>,-?[0-9]+)*\z/) {
2393 $coords = [split /,/, $value];
2394 } else {
2395 $self->{onerror}->(node => $attr,
2396 type => 'coords:syntax error');
2397 }
2398 },
2399 target => $HTMLTargetAttrChecker,
2400 href => $HTMLURIAttrChecker,
2401 ping => $HTMLSpaceURIsAttrChecker,
2402 rel => sub { $HTMLLinkTypesAttrChecker->(1, @_) },
2403 media => $HTMLMQAttrChecker,
2404 hreflang => $HTMLLanguageTagAttrChecker,
2405 type => $HTMLIMTAttrChecker,
2406 }->{$attr_ln};
2407 if ($checker) {
2408 $attr{$attr_ln} = $attr;
2409 } else {
2410 $checker = $HTMLAttrChecker->{$attr_ln};
2411 }
2412 }
2413 $checker ||= $AttrChecker->{$attr_ns}->{$attr_ln}
2414 || $AttrChecker->{$attr_ns}->{''};
2415 if ($checker) {
2416 $checker->($self, $attr) if ref $checker;
2417 } else {
2418 $self->{onerror}->(node => $attr, level => 'unsupported',
2419 type => 'attribute');
2420 ## ISSUE: No comformance createria for unknown attributes in the spec
2421 }
2422 }
2423
2424 if (defined $attr{href}) {
2425 unless (defined $attr{alt}) {
2426 $self->{onerror}->(node => $todo->{node},
2427 type => 'attribute missing:alt');
2428 }
2429 } else {
2430 for (qw/target ping rel media hreflang type alt/) {
2431 if (defined $attr{$_}) {
2432 $self->{onerror}->(node => $attr{$_},
2433 type => 'attribute not allowed');
2434 }
2435 }
2436 }
2437
2438 my $shape = 'rectangle';
2439 if (defined $attr{shape}) {
2440 $shape = {
2441 circ => 'circle', circle => 'circle',
2442 default => 'default',
2443 poly => 'polygon', polygon => 'polygon',
2444 rect => 'rectangle', rectangle => 'rectangle',
2445 }->{lc $attr{shape}->value} || 'rectangle';
2446 ## TODO: ASCII lowercase?
2447 }
2448
2449 if ($shape eq 'circle') {
2450 if (defined $attr{coords}) {
2451 if (defined $coords) {
2452 if (@$coords == 3) {
2453 if ($coords->[2] < 0) {
2454 $self->{onerror}->(node => $attr{coords},
2455 type => 'coords:out of range:2');
2456 }
2457 } else {
2458 $self->{onerror}->(node => $attr{coords},
2459 type => 'coords:number:3:'.@$coords);
2460 }
2461 } else {
2462 ## NOTE: A syntax error has been reported.
2463 }
2464 } else {
2465 $self->{onerror}->(node => $todo->{node},
2466 type => 'attribute missing:coords');
2467 }
2468 } elsif ($shape eq 'default') {
2469 if (defined $attr{coords}) {
2470 $self->{onerror}->(node => $attr{coords},
2471 type => 'attribute not allowed');
2472 }
2473 } elsif ($shape eq 'polygon') {
2474 if (defined $attr{coords}) {
2475 if (defined $coords) {
2476 if (@$coords >= 6) {
2477 unless (@$coords % 2 == 0) {
2478 $self->{onerror}->(node => $attr{coords},
2479 type => 'coords:number:even:'.@$coords);
2480 }
2481 } else {
2482 $self->{onerror}->(node => $attr{coords},
2483 type => 'coords:number:>=6:'.@$coords);
2484 }
2485 } else {
2486 ## NOTE: A syntax error has been reported.
2487 }
2488 } else {
2489 $self->{onerror}->(node => $todo->{node},
2490 type => 'attribute missing:coords');
2491 }
2492 } elsif ($shape eq 'rectangle') {
2493 if (defined $attr{coords}) {
2494 if (defined $coords) {
2495 if (@$coords == 4) {
2496 unless ($coords->[0] < $coords->[2]) {
2497 $self->{onerror}->(node => $attr{coords},
2498 type => 'coords:out of range:0');
2499 }
2500 unless ($coords->[1] < $coords->[3]) {
2501 $self->{onerror}->(node => $attr{coords},
2502 type => 'coords:out of range:1');
2503 }
2504 } else {
2505 $self->{onerror}->(node => $attr{coords},
2506 type => 'coords:number:4:'.@$coords);
2507 }
2508 } else {
2509 ## NOTE: A syntax error has been reported.
2510 }
2511 } else {
2512 $self->{onerror}->(node => $todo->{node},
2513 type => 'attribute missing:coords');
2514 }
2515 }
2516 },
2517 checker => $HTMLEmptyChecker,
2518 };
2519 ## TODO: only in map
2520
2521 $Element->{$HTML_NS}->{table} = {
2522 attrs_checker => $GetHTMLAttrsChecker->({}),
2523 checker => sub {
2524 my ($self, $todo) = @_;
2525 my $el = $todo->{node};
2526 my $new_todos = [];
2527 my @nodes = (@{$el->child_nodes});
2528
2529 my $phase = 'before caption';
2530 my $has_tfoot;
2531 while (@nodes) {
2532 my $node = shift @nodes;
2533 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
2534
2535 my $nt = $node->node_type;
2536 if ($nt == 1) {
2537 my $node_ns = $node->namespace_uri;
2538 $node_ns = '' unless defined $node_ns;
2539 my $node_ln = $node->manakai_local_name;
2540 ## NOTE: |minuses| list is not checked since redundant
2541 if ($phase eq 'in tbodys') {
2542 if ($node_ns eq $HTML_NS and $node_ln eq 'tbody') {
2543 #$phase = 'in tbodys';
2544 } elsif (not $has_tfoot and
2545 $node_ns eq $HTML_NS and $node_ln eq 'tfoot') {
2546 $phase = 'after tfoot';
2547 $has_tfoot = 1;
2548 } else {
2549 $self->{onerror}->(node => $node, type => 'element not allowed');
2550 }
2551 } elsif ($phase eq 'in trs') {
2552 if ($node_ns eq $HTML_NS and $node_ln eq 'tr') {
2553 #$phase = 'in trs';
2554 } elsif (not $has_tfoot and
2555 $node_ns eq $HTML_NS and $node_ln eq 'tfoot') {
2556 $phase = 'after tfoot';
2557 $has_tfoot = 1;
2558 } else {
2559 $self->{onerror}->(node => $node, type => 'element not allowed');
2560 }
2561 } elsif ($phase eq 'after thead') {
2562 if ($node_ns eq $HTML_NS and $node_ln eq 'tbody') {
2563 $phase = 'in tbodys';
2564 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'tr') {
2565 $phase = 'in trs';
2566 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'tfoot') {
2567 $phase = 'in tbodys';
2568 $has_tfoot = 1;
2569 } else {
2570 $self->{onerror}->(node => $node, type => 'element not allowed');
2571 }
2572 } elsif ($phase eq 'in colgroup') {
2573 if ($node_ns eq $HTML_NS and $node_ln eq 'colgroup') {
2574 $phase = 'in colgroup';
2575 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'thead') {
2576 $phase = 'after thead';
2577 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'tbody') {
2578 $phase = 'in tbodys';
2579 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'tr') {
2580 $phase = 'in trs';
2581 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'tfoot') {
2582 $phase = 'in tbodys';
2583 $has_tfoot = 1;
2584 } else {
2585 $self->{onerror}->(node => $node, type => 'element not allowed');
2586 }
2587 } elsif ($phase eq 'before caption') {
2588 if ($node_ns eq $HTML_NS and $node_ln eq 'caption') {
2589 $phase = 'in colgroup';
2590 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'colgroup') {
2591 $phase = 'in colgroup';
2592 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'thead') {
2593 $phase = 'after thead';
2594 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'tbody') {
2595 $phase = 'in tbodys';
2596 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'tr') {
2597 $phase = 'in trs';
2598 } elsif ($node_ns eq $HTML_NS and $node_ln eq 'tfoot') {
2599 $phase = 'in tbodys';
2600 $has_tfoot = 1;
2601 } else {
2602 $self->{onerror}->(node => $node, type => 'element not allowed');
2603 }
2604 } else { # after tfoot
2605 $self->{onerror}->(node => $node, type => 'element not allowed');
2606 }
2607 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
2608 unshift @nodes, @$sib;
2609 push @$new_todos, @$ch;
2610 } elsif ($nt == 3 or $nt == 4) {
2611 if ($node->data =~ /[^\x09-\x0D\x20]/) {
2612 $self->{onerror}->(node => $node, type => 'character not allowed');
2613 }
2614 } elsif ($nt == 5) {
2615 unshift @nodes, @{$node->child_nodes};
2616 }
2617 }
2618
2619 ## Table model errors
2620 require Whatpm::HTMLTable;
2621 Whatpm::HTMLTable->form_table ($todo->{node}, sub {
2622 my %opt = @_;
2623 $self->{onerror}->(type => 'table:'.$opt{type}, node => $opt{node});
2624 });
2625 push @{$self->{return}->{table}}, $todo->{node};
2626
2627 return ($new_todos);
2628 },
2629 };
2630
2631 $Element->{$HTML_NS}->{caption} = {
2632 attrs_checker => $GetHTMLAttrsChecker->({}),
2633 checker => $HTMLSignificantStrictlyInlineChecker,
2634 };
2635
2636 $Element->{$HTML_NS}->{colgroup} = {
2637 attrs_checker => $GetHTMLAttrsChecker->({
2638 span => $GetHTMLNonNegativeIntegerAttrChecker->(sub { shift > 0 }),
2639 ## NOTE: Defined only if "the |colgroup| element contains no |col| elements"
2640 ## TODO: "attribute not supported" if |col|.
2641 ## ISSUE: MUST NOT if any |col|?
2642 ## ISSUE: MUST NOT for |<colgroup span="1"><any><col/></any></colgroup>| (though non-conforming)?
2643 }),
2644 checker => sub {
2645 my ($self, $todo) = @_;
2646 my $el = $todo->{node};
2647 my $new_todos = [];
2648 my @nodes = (@{$el->child_nodes});
2649
2650 while (@nodes) {
2651 my $node = shift @nodes;
2652 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
2653
2654 my $nt = $node->node_type;
2655 if ($nt == 1) {
2656 my $node_ns = $node->namespace_uri;
2657 $node_ns = '' unless defined $node_ns;
2658 my $node_ln = $node->manakai_local_name;
2659 ## NOTE: |minuses| list is not checked since redundant
2660 unless ($node_ns eq $HTML_NS and $node_ln eq 'col') {
2661 $self->{onerror}->(node => $node, type => 'element not allowed');
2662 }
2663 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
2664 unshift @nodes, @$sib;
2665 push @$new_todos, @$ch;
2666 } elsif ($nt == 3 or $nt == 4) {
2667 if ($node->data =~ /[^\x09-\x0D\x20]/) {
2668 $self->{onerror}->(node => $node, type => 'character not allowed');
2669 }
2670 } elsif ($nt == 5) {
2671 unshift @nodes, @{$node->child_nodes};
2672 }
2673 }
2674 return ($new_todos);
2675 },
2676 };
2677
2678 $Element->{$HTML_NS}->{col} = {
2679 attrs_checker => $GetHTMLAttrsChecker->({
2680 span => $GetHTMLNonNegativeIntegerAttrChecker->(sub { shift > 0 }),
2681 }),
2682 checker => $HTMLEmptyChecker,
2683 };
2684
2685 $Element->{$HTML_NS}->{tbody} = {
2686 attrs_checker => $GetHTMLAttrsChecker->({}),
2687 checker => sub {
2688 my ($self, $todo) = @_;
2689 my $el = $todo->{node};
2690 my $new_todos = [];
2691 my @nodes = (@{$el->child_nodes});
2692
2693 my $has_tr;
2694 while (@nodes) {
2695 my $node = shift @nodes;
2696 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
2697
2698 my $nt = $node->node_type;
2699 if ($nt == 1) {
2700 my $node_ns = $node->namespace_uri;
2701 $node_ns = '' unless defined $node_ns;
2702 my $node_ln = $node->manakai_local_name;
2703 ## NOTE: |minuses| list is not checked since redundant
2704 if ($node_ns eq $HTML_NS and $node_ln eq 'tr') {
2705 $has_tr = 1;
2706 } else {
2707 $self->{onerror}->(node => $node, type => 'element not allowed');
2708 }
2709 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
2710 unshift @nodes, @$sib;
2711 push @$new_todos, @$ch;
2712 } elsif ($nt == 3 or $nt == 4) {
2713 if ($node->data =~ /[^\x09-\x0D\x20]/) {
2714 $self->{onerror}->(node => $node, type => 'character not allowed');
2715 }
2716 } elsif ($nt == 5) {
2717 unshift @nodes, @{$node->child_nodes};
2718 }
2719 }
2720 unless ($has_tr) {
2721 $self->{onerror}->(node => $el, type => 'child element missing:tr');
2722 }
2723 return ($new_todos);
2724 },
2725 };
2726
2727 $Element->{$HTML_NS}->{thead} = {
2728 attrs_checker => $GetHTMLAttrsChecker->({}),
2729 checker => $Element->{$HTML_NS}->{tbody}->{checker},
2730 };
2731
2732 $Element->{$HTML_NS}->{tfoot} = {
2733 attrs_checker => $GetHTMLAttrsChecker->({}),
2734 checker => $Element->{$HTML_NS}->{tbody}->{checker},
2735 };
2736
2737 $Element->{$HTML_NS}->{tr} = {
2738 attrs_checker => $GetHTMLAttrsChecker->({}),
2739 checker => sub {
2740 my ($self, $todo) = @_;
2741 my $el = $todo->{node};
2742 my $new_todos = [];
2743 my @nodes = (@{$el->child_nodes});
2744
2745 my $has_td;
2746 while (@nodes) {
2747 my $node = shift @nodes;
2748 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
2749
2750 my $nt = $node->node_type;
2751 if ($nt == 1) {
2752 my $node_ns = $node->namespace_uri;
2753 $node_ns = '' unless defined $node_ns;
2754 my $node_ln = $node->manakai_local_name;
2755 ## NOTE: |minuses| list is not checked since redundant
2756 if ($node_ns eq $HTML_NS and ($node_ln eq 'td' or $node_ln eq 'th')) {
2757 $has_td = 1;
2758 } else {
2759 $self->{onerror}->(node => $node, type => 'element not allowed');
2760 }
2761 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
2762 unshift @nodes, @$sib;
2763 push @$new_todos, @$ch;
2764 } elsif ($nt == 3 or $nt == 4) {
2765 if ($node->data =~ /[^\x09-\x0D\x20]/) {
2766 $self->{onerror}->(node => $node, type => 'character not allowed');
2767 }
2768 } elsif ($nt == 5) {
2769 unshift @nodes, @{$node->child_nodes};
2770 }
2771 }
2772 unless ($has_td) {
2773 $self->{onerror}->(node => $el, type => 'child element missing:td|th');
2774 }
2775 return ($new_todos);
2776 },
2777 };
2778
2779 $Element->{$HTML_NS}->{td} = {
2780 attrs_checker => $GetHTMLAttrsChecker->({
2781 colspan => $GetHTMLNonNegativeIntegerAttrChecker->(sub { shift > 0 }),
2782 rowspan => $GetHTMLNonNegativeIntegerAttrChecker->(sub { shift > 0 }),
2783 }),
2784 checker => $HTMLBlockOrInlineChecker,
2785 };
2786
2787 $Element->{$HTML_NS}->{th} = {
2788 attrs_checker => $GetHTMLAttrsChecker->({
2789 colspan => $GetHTMLNonNegativeIntegerAttrChecker->(sub { shift > 0 }),
2790 rowspan => $GetHTMLNonNegativeIntegerAttrChecker->(sub { shift > 0 }),
2791 scope => $GetHTMLEnumeratedAttrChecker
2792 ->({row => 1, col => 1, rowgroup => 1, colgroup => 1}),
2793 }),
2794 checker => $HTMLBlockOrInlineChecker,
2795 };
2796
2797 ## TODO: forms
2798
2799 $Element->{$HTML_NS}->{script} = {
2800 attrs_checker => sub {
2801 my ($self, $todo) = @_;
2802 $GetHTMLAttrsChecker->({
2803 src => $HTMLURIAttrChecker,
2804 defer => $GetHTMLBooleanAttrChecker->('defer'),
2805 async => $GetHTMLBooleanAttrChecker->('async'),
2806 type => $HTMLIMTAttrChecker,
2807 })->($self, $todo);
2808 if ($todo->{node}->has_attribute_ns (undef, 'defer')) {
2809 my $async_attr = $todo->{node}->get_attribute_node_ns (undef, 'async');
2810 if ($async_attr) {
2811 $self->{onerror}->(node => $async_attr,
2812 type => 'attribute not allowed'); # MUST NOT
2813 }
2814 }
2815 },
2816 checker => sub {
2817 my ($self, $todo) = @_;
2818
2819 if ($todo->{node}->has_attribute_ns (undef, 'src')) {
2820 return $HTMLEmptyChecker->($self, $todo);
2821 } else {
2822 ## NOTE: No content model conformance in HTML5 spec.
2823 my $type = $todo->{node}->get_attribute_ns (undef, 'type');
2824 my $language = $todo->{node}->get_attribute_ns (undef, 'language');
2825 if ((defined $type and $type eq '') or
2826 (defined $language and $language eq '')) {
2827 $type = 'text/javascript';
2828 } elsif (defined $type) {
2829 #
2830 } elsif (defined $language) {
2831 $type = 'text/' . $language;
2832 } else {
2833 $type = 'text/javascript';
2834 }
2835 $self->{onerror}->(node => $todo->{node}, level => 'unsupported',
2836 type => 'script:'.$type); ## TODO: $type normalization
2837 return $AnyChecker->($self, $todo);
2838 }
2839 },
2840 };
2841
2842 ## NOTE: When script is disabled.
2843 $Element->{$HTML_NS}->{noscript} = {
2844 attrs_checker => $GetHTMLAttrsChecker->({}),
2845 checker => sub {
2846 my ($self, $todo) = @_;
2847
2848 my $end = $self->_add_minuses ({$HTML_NS => {noscript => 1}});
2849 my ($sib, $ch) = $HTMLBlockOrInlineChecker->($self, $todo);
2850 push @$sib, $end;
2851 return ($sib, $ch);
2852 },
2853 };
2854 ## TODO: noscript in head
2855 ## TODO: noscript in XHTML
2856
2857 $Element->{$HTML_NS}->{'event-source'} = {
2858 attrs_checker => $GetHTMLAttrsChecker->({
2859 src => $HTMLURIAttrChecker,
2860 }),
2861 checker => $HTMLEmptyChecker,
2862 };
2863
2864 $Element->{$HTML_NS}->{details} = {
2865 attrs_checker => $GetHTMLAttrsChecker->({
2866 open => $GetHTMLBooleanAttrChecker->('open'),
2867 }),
2868 checker => sub {
2869 my ($self, $todo) = @_;
2870
2871 my $end = $self->_add_minuses ({$HTML_NS => {a => 1, datagrid => 1}});
2872 my ($sib, $ch)
2873 = $GetHTMLZeroOrMoreThenBlockOrInlineChecker->($HTML_NS, 'legend')
2874 ->($self, $todo);
2875 push @$sib, $end;
2876 return ($sib, $ch);
2877 },
2878 };
2879
2880 $Element->{$HTML_NS}->{datagrid} = {
2881 attrs_checker => $GetHTMLAttrsChecker->({
2882 disabled => $GetHTMLBooleanAttrChecker->('disabled'),
2883 multiple => $GetHTMLBooleanAttrChecker->('multiple'),
2884 }),
2885 checker => sub {
2886 my ($self, $todo) = @_;
2887 my $el = $todo->{node};
2888 my $new_todos = [];
2889 my @nodes = (@{$el->child_nodes});
2890
2891 my $end = $self->_add_minuses ({$HTML_NS => {a => 1, datagrid => 1}});
2892
2893 ## Block-table Block* | table | select | datalist | Empty
2894 my $mode = 'any';
2895 while (@nodes) {
2896 my $node = shift @nodes;
2897 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
2898
2899 my $nt = $node->node_type;
2900 if ($nt == 1) {
2901 my $node_ns = $node->namespace_uri;
2902 $node_ns = '' unless defined $node_ns;
2903 my $node_ln = $node->manakai_local_name;
2904 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
2905 if ($mode eq 'block') {
2906 $not_allowed = 1
2907 unless $HTMLBlockLevelElements->{$node_ns}->{$node_ln};
2908 } elsif ($mode eq 'any') {
2909 if ($node_ns eq $HTML_NS and
2910 {table => 1, select => 1, datalist => 1}->{$node_ln}) {
2911 $mode = 'none';
2912 } elsif ($HTMLBlockLevelElements->{$node_ns}->{$node_ln}) {
2913 $mode = 'block';
2914 } else {
2915 $not_allowed = 1;
2916 }
2917 } else {
2918 $not_allowed = 1;
2919 }
2920 $self->{onerror}->(node => $node, type => 'element not allowed')
2921 if $not_allowed;
2922 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
2923 unshift @nodes, @$sib;
2924 push @$new_todos, @$ch;
2925 } elsif ($nt == 3 or $nt == 4) {
2926 if ($node->data =~ /[^\x09-\x0D\x20]/) {
2927 $self->{onerror}->(node => $node, type => 'character not allowed');
2928 }
2929 } elsif ($nt == 5) {
2930 unshift @nodes, @{$node->child_nodes};
2931 }
2932 }
2933
2934 push @$new_todos, $end;
2935 return ($new_todos);
2936 },
2937 };
2938
2939 $Element->{$HTML_NS}->{command} = {
2940 attrs_checker => $GetHTMLAttrsChecker->({
2941 checked => $GetHTMLBooleanAttrChecker->('checked'),
2942 default => $GetHTMLBooleanAttrChecker->('default'),
2943 disabled => $GetHTMLBooleanAttrChecker->('disabled'),
2944 hidden => $GetHTMLBooleanAttrChecker->('hidden'),
2945 icon => $HTMLURIAttrChecker,
2946 label => sub { }, ## NOTE: No conformance creteria
2947 radiogroup => sub { }, ## NOTE: No conformance creteria
2948 ## NOTE: |title| has special semantics, but no syntactical difference
2949 type => sub {
2950 my ($self, $attr) = @_;
2951 my $value = $attr->value;
2952 unless ({command => 1, checkbox => 1, radio => 1}->{$value}) {
2953 $self->{onerror}->(node => $attr, type => 'attribute value not allowed');
2954 }
2955 },
2956 }),
2957 checker => $HTMLEmptyChecker,
2958 };
2959
2960 $Element->{$HTML_NS}->{menu} = {
2961 attrs_checker => $GetHTMLAttrsChecker->({
2962 autosubmit => $GetHTMLBooleanAttrChecker->('autosubmit'),
2963 id => sub {
2964 ## NOTE: same as global |id=""|, with |$self->{menu}| registeration
2965 my ($self, $attr) = @_;
2966 my $value = $attr->value;
2967 if (length $value > 0) {
2968 if ($self->{id}->{$value}) {
2969 $self->{onerror}->(node => $attr, type => 'duplicate ID');
2970 push @{$self->{id}->{$value}}, $attr;
2971 } else {
2972 $self->{id}->{$value} = [$attr];
2973 }
2974 } else {
2975 ## NOTE: MUST contain at least one character
2976 $self->{onerror}->(node => $attr, type => 'empty attribute value');
2977 }
2978 if ($value =~ /[\x09-\x0D\x20]/) {
2979 $self->{onerror}->(node => $attr, type => 'space in ID');
2980 }
2981 $self->{menu}->{$value} ||= $attr;
2982 ## ISSUE: <menu id=""><p contextmenu=""> match?
2983 },
2984 label => sub { }, ## NOTE: No conformance creteria
2985 type => $GetHTMLEnumeratedAttrChecker->({context => 1, toolbar => 1}),
2986 }),
2987 checker => sub {
2988 my ($self, $todo) = @_;
2989 my $el = $todo->{node};
2990 my $new_todos = [];
2991 my @nodes = (@{$el->child_nodes});
2992
2993 my $content = 'li or inline';
2994 while (@nodes) {
2995 my $node = shift @nodes;
2996 $self->_remove_minuses ($node) and next if ref $node eq 'HASH';
2997
2998 my $nt = $node->node_type;
2999 if ($nt == 1) {
3000 my $node_ns = $node->namespace_uri;
3001 $node_ns = '' unless defined $node_ns;
3002 my $node_ln = $node->manakai_local_name;
3003 my $not_allowed = $self->{minuses}->{$node_ns}->{$node_ln};
3004 if ($node_ns eq $HTML_NS and $node_ln eq 'li') {
3005 if ($content eq 'inline') {
3006 $not_allowed = 1;
3007 } elsif ($content eq 'li or inline') {
3008 $content = 'li';
3009 }
3010 } else {
3011 if ($HTMLStrictlyInlineLevelElements->{$node_ns}->{$node_ln} or
3012 $HTMLStructuredInlineLevelElements->{$node_ns}->{$node_ln}) {
3013 $content = 'inline';
3014 } else {
3015 $not_allowed = 1;
3016 }
3017 }
3018 $self->{onerror}->(node => $node, type => 'element not allowed')
3019 if $not_allowed;
3020 my ($sib, $ch) = $self->_check_get_children ($node, $todo);
3021 unshift @nodes, @$sib;
3022 push @$new_todos, @$ch;
3023 } elsif ($nt == 3 or $nt == 4) {
3024 if ($node->data =~ /[^\x09-\x0D\x20]/) {
3025 if ($content eq 'li') {
3026 $self->{onerror}->(node => $node, type => 'character not allowed');
3027 } elsif ($content eq 'li or inline') {
3028 $content = 'inline';
3029 }
3030 }
3031 } elsif ($nt == 5) {
3032 unshift @nodes, @{$node->child_nodes};
3033 }
3034 }
3035
3036 for (@$new_todos) {
3037 $_->{inline} = 1;
3038 }
3039 return ($new_todos);
3040 },
3041 };
3042
3043 $Element->{$HTML_NS}->{legend} = {
3044 attrs_checker => $GetHTMLAttrsChecker->({}),
3045 checker => sub {
3046 my ($self, $todo) = @_;
3047
3048 my $parent = $todo->{node}->manakai_parent_element;
3049 if (defined $parent) {
3050 my $nsuri = $parent->namespace_uri;
3051 $nsuri = '' unless defined $nsuri;
3052 my $ln = $parent->manakai_local_name;
3053 if ($nsuri eq $HTML_NS and $ln eq 'figure') {
3054 return $HTMLInlineChecker->($self, $todo);
3055 } else {
3056 return $HTMLSignificantStrictlyInlineChecker->($self, $todo);
3057 }
3058 } else {
3059 return $HTMLInlineChecker->($self, $todo);
3060 }
3061
3062 ## ISSUE: Content model is defined only for fieldset/legend,
3063 ## details/legend, and figure/legend.
3064 },
3065 };
3066
3067 $Element->{$HTML_NS}->{div} = {
3068 attrs_checker => $GetHTMLAttrsChecker->({}),
3069 checker => $GetHTMLZeroOrMoreThenBlockOrInlineChecker->($HTML_NS, 'style'),
3070 };
3071
3072 $Element->{$HTML_NS}->{font} = {
3073 attrs_checker => $GetHTMLAttrsChecker->({}), ## TODO
3074 checker => $HTMLTransparentChecker,
3075 };
3076
3077 sub check_document ($$$) {
3078 my ($self, $doc, $onerror) = @_;
3079 $self = bless {}, $self unless ref $self;
3080 $self->{onerror} = $onerror;
3081
3082 my $docel = $doc->document_element;
3083 unless (defined $docel) {
3084 ## ISSUE: Should we check content of Document node?
3085 $onerror->(node => $doc, type => 'no document element');
3086 ## ISSUE: Is this non-conforming (to what spec)? Or just a warning?
3087 return {
3088 class => {},
3089 id => {}, table => [], term => {},
3090 };
3091 }
3092
3093 ## ISSUE: Unexpanded entity references and HTML5 conformance
3094
3095 my $docel_nsuri = $docel->namespace_uri;
3096 $docel_nsuri = '' unless defined $docel_nsuri;
3097 my $docel_def = $Element->{$docel_nsuri}->{$docel->manakai_local_name} ||
3098 $Element->{$docel_nsuri}->{''} ||
3099 $ElementDefault;
3100 if ($docel_def->{is_root}) {
3101 #
3102 } else {
3103 $onerror->(node => $docel, type => 'element not allowed');
3104 }
3105
3106 ## TODO: Check for other items other than document element
3107 ## (second (errorous) element, text nodes, PI nodes, doctype nodes)
3108
3109 return $self->check_element ($docel, $onerror);
3110 } # check_document
3111
3112 sub check_element ($$$) {
3113 my ($self, $el, $onerror) = @_;
3114 $self = bless {}, $self unless ref $self;
3115 $self->{onerror} = $onerror;
3116
3117 $self->{minuses} = {};
3118 $self->{id} = {};
3119 $self->{term} = {};
3120 $self->{usemap} = [];
3121 $self->{contextmenu} = [];
3122 $self->{map} = {};
3123 $self->{menu} = {};
3124 $self->{has_link_type} = {};
3125 $self->{return} = {
3126 class => {},
3127 id => $self->{id}, table => [], term => $self->{term},
3128 };
3129
3130 my @todo = ({type => 'element', node => $el});
3131 while (@todo) {
3132 my $todo = shift @todo;
3133 if ($todo->{type} eq 'element') {
3134 my $prefix = $todo->{node}->prefix;
3135 if (defined $prefix and $prefix eq 'xmlns') {
3136 $self->{onerror}
3137 ->(node => $todo->{node}, level => 'NC',
3138 type => 'Reserved Prefixes and Namespace Names:<xmlns:>');
3139 }
3140 my $nsuri = $todo->{node}->namespace_uri;
3141 $nsuri = '' unless defined $nsuri;
3142 my $ln = $todo->{node}->manakai_local_name;
3143 my $eldef = $Element->{$nsuri}->{$ln} ||
3144 $Element->{$nsuri}->{''} ||
3145 $ElementDefault;
3146 $eldef->{attrs_checker}->($self, $todo);
3147 my ($new_todos) = $eldef->{checker}->($self, $todo);
3148 unshift @todo, @$new_todos;
3149 } elsif ($todo->{type} eq 'element-attributes') {
3150 my $prefix = $todo->{node}->prefix;
3151 if (defined $prefix and $prefix eq 'xmlns') {
3152 $self->{onerror}
3153 ->(node => $todo->{node}, level => 'NC',
3154 type => 'Reserved Prefixes and Namespace Names:<xmlns:>');
3155 }
3156 my $nsuri = $todo->{node}->namespace_uri;
3157 $nsuri = '' unless defined $nsuri;
3158 my $ln = $todo->{node}->manakai_local_name;
3159 my $eldef = $Element->{$nsuri}->{$ln} ||
3160 $Element->{$nsuri}->{''} ||
3161 $ElementDefault;
3162 $eldef->{attrs_checker}->($self, $todo);
3163 } elsif ($todo->{type} eq 'plus') {
3164 $self->_remove_minuses ($todo);
3165 } elsif ($todo->{type} eq 'code') {
3166 $todo->{code}->();
3167 } else {
3168 die "$0: Internal error: Unsupported checking action type |$todo->{type}|";
3169 }
3170 }
3171
3172 for (@{$self->{usemap}}) {
3173 unless ($self->{map}->{$_->[0]}) {
3174 $self->{onerror}->(node => $_->[1], type => 'no referenced map');
3175 }
3176 }
3177
3178 for (@{$self->{contextmenu}}) {
3179 unless ($self->{menu}->{$_->[0]}) {
3180 $self->{onerror}->(node => $_->[1], type => 'no referenced menu');
3181 }
3182 }
3183
3184 delete $self->{minuses};
3185 delete $self->{onerror};
3186 delete $self->{id};
3187 delete $self->{usemap};
3188 delete $self->{map};
3189 return $self->{return};
3190 } # check_element
3191
3192 sub _add_minuses ($@) {
3193 my $self = shift;
3194 my $r = {};
3195 for my $list (@_) {
3196 for my $ns (keys %$list) {
3197 for my $ln (keys %{$list->{$ns}}) {
3198 unless ($self->{minuses}->{$ns}->{$ln}) {
3199 $self->{minuses}->{$ns}->{$ln} = 1;
3200 $r->{$ns}->{$ln} = 1;
3201 }
3202 }
3203 }
3204 }
3205 return {type => 'plus', list => $r};
3206 } # _add_minuses
3207
3208 sub _remove_minuses ($$) {
3209 my ($self, $todo) = @_;
3210 for my $ns (keys %{$todo->{list}}) {
3211 for my $ln (keys %{$todo->{list}->{$ns}}) {
3212 delete $self->{minuses}->{$ns}->{$ln} if $todo->{list}->{$ns}->{$ln};
3213 }
3214 }
3215 1;
3216 } # _remove_minuses
3217
3218 sub _check_get_children ($$$) {
3219 my ($self, $node, $parent_todo) = @_;
3220 my $new_todos = [];
3221 my $sib = [];
3222 TP: {
3223 my $node_ns = $node->namespace_uri;
3224 $node_ns = '' unless defined $node_ns;
3225 my $node_ln = $node->manakai_local_name;
3226 if ($node_ns eq $HTML_NS) {
3227 if ($node_ln eq 'noscript') {
3228 my $end = $self->_add_minuses ({$HTML_NS, {noscript => 1}});
3229 push @$sib, $end;
3230 }
3231 }
3232 ## TODO: |noscript| is not a transparent element in |head|.
3233 if ($HTMLTransparentElements->{$node_ns}->{$node_ln}) {
3234 unshift @$sib, @{$node->child_nodes};
3235 push @$new_todos, {type => 'element-attributes', node => $node};
3236 last TP;
3237 }
3238 if ($node_ns eq $HTML_NS and ($node_ln eq 'video' or $node_ln eq 'audio')) {
3239 if ($node->has_attribute_ns (undef, 'src')) {
3240 unshift @$sib, @{$node->child_nodes};
3241 push @$new_todos, {type => 'element-attributes', node => $node};
3242 last TP;
3243 } else {
3244 my @cn = @{$node->child_nodes};
3245 CN: while (@cn) {
3246 my $cn = shift @cn;
3247 my $cnt = $cn->node_type;
3248 if ($cnt == 1) {
3249 my $cn_nsuri = $cn->namespace_uri;
3250 $cn_nsuri = '' unless defined $cn_nsuri;
3251 if ($cn_nsuri eq $HTML_NS and $cn->manakai_local_name eq 'source') {
3252 #
3253 } else {
3254 last CN;
3255 }
3256 } elsif ($cnt == 3 or $cnt == 4) {
3257 if ($cn->data =~ /[^\x09-\x0D\x20]/) {
3258 last CN;
3259 }
3260 }
3261 } # CN
3262 unshift @$sib, @cn;
3263 }
3264 }
3265 push @$new_todos, {type => 'element', node => $node};
3266 } # TP
3267
3268 for my $new_todo (@$new_todos) {
3269 $new_todo->{flag} = {%{$parent_todo->{flag} or {}}};
3270 }
3271
3272 return ($sib, $new_todos);
3273 } # _check_get_children
3274
3275 1;
3276 # $Date: 2007/07/29 05:20:12 $

[email protected]
ViewVC Help
Powered by ViewVC 1.1.24