File Coverage

blib/lib/Web/Microformats2/Parser.pm
Criterion Covered Total %
statement 339 343 98.8
branch 170 184 92.3
condition 41 48 85.4
subroutine 29 29 100.0
pod 1 2 50.0
total 580 606 95.7


line stmt bran cond sub pod time code
1             package Web::Microformats2::Parser;
2              
3 2     2   1517 use Moo;
  2         23739  
  2         11  
4 2     2   4344 use Types::Standard qw(InstanceOf);
  2         153464  
  2         20  
5 2     2   2965 use HTML::TreeBuilder::XPath;
  2         135593  
  2         21  
6 2     2   139 use HTML::Entities;
  2         6  
  2         146  
7 2     2   38 use v5.10;
  2         8  
8 2     2   13 use Scalar::Util qw(blessed);
  2         4  
  2         99  
9 2     2   743 use JSON;
  2         8566  
  2         17  
10 2     2   1517 use DateTime::Format::ISO8601;
  2         1186576  
  2         92  
11 2     2   1350 use URI;
  2         9388  
  2         68  
12 2     2   16 use Carp;
  2         6  
  2         121  
13              
14 2     2   1139 use Web::Microformats2::Item;
  2         7  
  2         84  
15 2     2   931 use Web::Microformats2::Document;
  2         6  
  2         84  
16              
17 2     2   1091 use Readonly;
  2         8011  
  2         8176  
18              
19             has 'url_context' => (
20             is => 'rw',
21             isa => InstanceOf['URI'],
22             coerce => sub { URI->new( $_[0] ) },
23             lazy => 1,
24             clearer => '_clear_url_context',
25             default => sub { URI->new( 'http://example.com/' ) },
26             );
27              
28             sub parse {
29 74     74 1 313212 my $self = shift;
30 74         276 my ( $html, %args ) = @_;
31              
32 74         249 $self->_clear;
33 74 50       723 if ( $args{ url_context } ) {
34 0         0 $self->url_context( $args{url_context} );
35             }
36              
37 74         618 my $tree = HTML::TreeBuilder::XPath->new;
38 74         20052 $tree->ignore_unknown( 0 );
39 74         904 $tree->no_space_compacting( 1 );
40 74         728 $tree->ignore_ignorable_whitespace( 0 );
41 74         716 $tree->no_expand_entities( 1 );
42              
43             # Adding HTML5 elements because it's 2018.
44 74         796 foreach (qw(article aside details figcaption figure footer header main mark nav section summary time)) {
45 962         1735 $HTML::TreeBuilder::isBodyElement{$_}=1;
46             }
47              
48 74         1020 $tree->parse( $html );
49              
50 74 100       196622 if ( my $base_url = $tree->findvalue( './/base/@href' ) ) {
51 6         8515 $self->url_context( $base_url );
52             }
53              
54 74         96739 my $document = Web::Microformats2::Document->new;
55 74         4607 $self->analyze_element( $document, $tree );
56 74         2002 return $document;
57             }
58              
59             # analyze_element: Recursive method that scans an element for new microformat
60             # definitions (h-*) or properties (u|dt|e|p-*) and then does the right thing.
61             # It also builds up the MF2 document's rels and rel-urls as it goes.
62             sub analyze_element {
63 1913     1913 0 2841 my $self = shift;
64 1913         3486 my ( $document, $element, $current_item ) = @_;
65              
66 1913 100 66     8635 return unless blessed( $element) && $element->isa( 'HTML::Element' );
67              
68 898         2609 $self->_add_element_rels_to_mf2_document( $element, $document );
69              
70 898         7019 my $mf2_attrs = $self->_tease_out_mf2_attrs( $element );
71              
72 898         1831 my $h_attrs = delete $mf2_attrs->{h};
73 898         1362 my $new_item;
74 898 100       1862 if ( $h_attrs->[0] ) {
75 136         3260 $new_item = Web::Microformats2::Item->new( {
76             types => $h_attrs,
77             parent => $current_item,
78             } );
79 136         5134 $document->add_item( $new_item );
80 136 100       7642 unless ( $current_item ) {
81 94         1673 $document->add_top_level_item( $new_item );
82             }
83             }
84              
85 898         8038 while (my ($mf2_type, $properties_ref ) = each( %$mf2_attrs ) ) {
86 3592 100       8096 next unless $current_item;
87 1924 100       2491 next unless @{ $properties_ref };
  1924         5577  
88 303 100       900 if ( $mf2_type eq 'p' ) {
    100          
    100          
    50          
89             # p-property:
90             # A catch-all generic property to store on the current
91             # MF2 item being defined.
92             # (If this same element begins an h-* microformat, we don't parse
93             # this p-* any further; instead we'll store the new item under
94             # this property name.)
95 190 100       421 unless ( $new_item ) {
96 158         316 for my $property ( @$properties_ref ) {
97 163         419 my $value = $self->_parse_property_value( $element );
98 163 100       380 if ( defined $value ) {
99 161         680 $current_item->add_property(
100             "p-$property",
101             $value,
102             );
103             }
104             }
105             }
106             }
107             elsif ( $mf2_type eq 'u' ) {
108             # u-property:
109             # Look for a URL in child attributes, and store it as a property.
110              
111             # (But not if a new h-format has been defined, in which case we'll
112             # just use the u-property's name to store it. Why would you do that
113             # instead of using a p-property? I don't know, but the tests demand
114             # it.)
115 44 100       116 unless ( $new_item ) {
116 42         116 for my $property ( @$properties_ref ) {
117 43         121 my $vcp_fragments_ref =
118             $self->_seek_value_class_pattern( $element );
119 43 100       113 if ( my $url = $self->_tease_out_url( $element ) ) {
    100          
    100          
120 38         159 $current_item->add_property( "u-$property", $url );
121             }
122             elsif ( @$vcp_fragments_ref ) {
123 2         14 $current_item->add_property(
124             "u-$property",
125             join q{}, @$vcp_fragments_ref,
126             )
127             }
128             elsif ( $url = $self->_tease_out_unlikely_url($element)) {
129 2         12 $current_item->add_property( "u-$property", $url );
130             }
131             else {
132 1         7 $current_item->add_property(
133             "u-$property",
134             _trim( $element->as_text ),
135             );
136             }
137             }
138             }
139             }
140             elsif ( $mf2_type eq 'e' ) {
141             # e-property:
142             # Create a struct with keys "html" and "value", and then
143             # store this in a new property.
144 18         44 for my $property ( @$properties_ref ) {
145 18         25 my %e_data;
146 18         57 for my $content_piece ( $element->content_list ) {
147              
148             # Make sure all URLs found in certain HTML attrs are
149             # absolute.
150 42 100       4808 if ( ref $content_piece ) {
151             # XXX This is probably a bit too loose about what tags
152             # these attrs can appear on.
153 14         58 for my $href_element ( $content_piece, $content_piece->findnodes('.//*[@href|@src]') ) {
154 22         23409 foreach ( qw( href src ) ) {
155 44         219 my $url = $href_element->attr($_);
156 44 100       481 if ( $url ) {
157 9         213 my $abs_url = URI->new_abs( $url, $self->url_context)->as_string;
158 9         1891 $href_element->attr( $_=> $abs_url );
159             }
160             }
161             }
162 14         73 $e_data{html} .= $content_piece->as_HTML( '<>&', undef, {} );
163              
164             }
165             else {
166              
167 28         82 $e_data{html} .= $content_piece;
168             }
169             }
170 18         455 $e_data{ value } = _trim (decode_entities( $element->as_text) );
171              
172             # The official tests specifically trim space-glyphs per se;
173             # all other trailing whitespace stays. Shrug.
174 18         178 $e_data{ html } =~ s/ +$//;
175              
176 18         89 $current_item->add_property( "e-$property", \%e_data );
177             }
178             }
179             elsif ( $mf2_type eq 'dt' ) {
180             # dt-property:
181             # Read a child attribute as an ISO-8601 date-time string.
182             # Store it as a property in the MF2 date-time representation format.
183 51         132 for my $property ( @$properties_ref ) {
184 51         71 my $dt_string;
185 51         119 my $vcp_fragments_ref =
186             $self->_seek_value_class_pattern( $element );
187 51 100       166 if ( @$vcp_fragments_ref ) {
    100          
    50          
188 22         78 $dt_string = $self->_format_datetime(join (q{T}, @$vcp_fragments_ref), $current_item);
189             }
190             elsif ( my $alt = $element->findvalue( './@datetime|@title|@value' ) ) {
191 26         23680 $dt_string = $alt;
192             }
193             elsif ( my $text = $element->as_trimmed_text ) {
194 3         2530 $dt_string = $text;
195             }
196 51 50       2391 if ( defined $dt_string ) {
197 51         207 $current_item->add_property(
198             "dt-$property",
199             $dt_string,
200             );
201             }
202             }
203             }
204             }
205              
206 898 100       1902 if ( $new_item ) {
207 136         494 for my $child_element ( $element->content_list ) {
208 587         3495 $self->analyze_element( $document, $child_element, $new_item );
209             }
210              
211             # Now that the new item's been recursively scanned, perform
212             # some post-processing.
213             # First, add any implied properties.
214 136         559 for my $impliable_property (qw(name photo url)) {
215 408 100       15370 unless ( $new_item->has_property( $impliable_property ) ) {
216 324         29257 my $method = "_set_implied_$impliable_property";
217 324         1084 $self->$method( $new_item, $element );
218             }
219             }
220              
221             # Put this onto the parent item's property-list, or its children-list,
222             # depending on context.
223 136         1979 my @item_properties;
224 136         243 for my $prefix (qw( u p ) ) {
225 272         375 push @item_properties, map { "$prefix-$_" } @{ $mf2_attrs->{$prefix} };
  37         146  
  272         596  
226             }
227 136 100 100     900 if ( $current_item && @item_properties ) {
    100          
228 33         74 for my $item_property ( @item_properties ) {
229             # We place a clone of the new item into the current item's
230             # property list, rather than the item itself. This allows for
231             # edge cases where the same item needs to go under multiple
232             # properties, but carry different 'value' attributes.
233 37         450 my $cloned_new_item =
234             bless { %$new_item }, ref $new_item;
235              
236 37         181 $current_item
237             ->add_property( "$item_property", $cloned_new_item );
238              
239             # Now add a "value" attribute to this new item, if appropriate,
240             # according to the MF2 spec.
241 37         79 my $value_attribute;
242 37 100       146 if ( $item_property =~ /^p-/ ) {
    50          
243 35 100       115 if ( my $name = $new_item->get_properties('name')->[0] ) {
244 28         64 $value_attribute = $name;
245             }
246             else {
247 7         21 $value_attribute =
248             $self->_parse_property_value( $element );
249             }
250             }
251             elsif ( $item_property =~ /^u-/ ) {
252 2         8 $value_attribute = $new_item->get_properties('url')->[0];
253             }
254              
255 37 50       1161 $cloned_new_item->value( $value_attribute ) if defined ($value_attribute);
256             }
257             }
258             elsif ($current_item) {
259 9         188 $current_item->add_child ( $new_item );
260             }
261              
262             }
263             else {
264 762         1990 for my $child_element ( $element->content_list ) {
265 1252         6299 $self->analyze_element( $document, $child_element, $current_item );
266             }
267             }
268             }
269              
270             sub _tease_out_mf2_attrs {
271 1053     1053   1649 my $self = shift;
272 1053         1767 my ( $element ) = @_;
273              
274 1053         1498 my %mf2_attrs;
275 1053         1911 foreach ( qw( h e u dt p ) ) {
276 5265         9852 $mf2_attrs{ $_ } = [];
277             }
278              
279 1053         2369 my $class_attr = $element->attr('class');
280 1053 100       11946 if ( $class_attr ) {
281 538         3443 while ($class_attr =~ /\b(h|e|u|dt|p)-([a-z]+(\-[a-z]+)*)($|\s)/g ) {
282 549         1372 my $mf2_type = $1;
283 549         967 my $mf2_attr = $2;
284              
285 549         748 push @{ $mf2_attrs{ $mf2_type } }, $mf2_attr;
  549         2523  
286             }
287             }
288              
289 1053         2376 return \%mf2_attrs;
290             }
291              
292             sub _tease_out_url {
293 43     43   72 my $self = shift;
294 43         80 my ( $element ) = @_;
295              
296 43         74 my $xpath;
297             my $url;
298 43 100       105 if ( $element->tag =~ /^(a|area|link)$/ ) {
    100          
    100          
    100          
299 26         257 $xpath = './@href';
300             }
301             elsif ( $element->tag =~ /^(img|audio)$/ ) {
302 10         163 $xpath = './@src';
303             }
304             elsif ( $element->tag eq 'video' ) {
305 1         20 $xpath = './@src|@poster';
306             }
307             elsif ( $element->tag eq 'object' ) {
308 1         30 $xpath = './@data';
309             }
310              
311 43 100       232 if ( $xpath ) {
312 38         115 $url = $element->findvalue( $xpath );
313             }
314              
315 43 100       16506 if ( defined $url ) {
316 38         972 $url = URI->new_abs( $url, $self->url_context )->as_string;
317             }
318              
319 43         10054 return $url;
320             }
321              
322             sub _tease_out_unlikely_url {
323 3     3   5 my $self = shift;
324 3         7 my ( $element ) = @_;
325              
326 3         6 my $xpath;
327             my $url;
328 3 100       7 if ( $element->tag eq 'abbr' ) {
    100          
329 1         10 $xpath = './@title';
330             }
331             elsif ( $element->tag =~ /^(data|input)$/ ) {
332 1         32 $xpath = './@value';
333             }
334              
335 3 100       18 if ( $xpath ) {
336 2         9 $url = $element->findvalue( $xpath );
337             }
338              
339 3         1593 return $url;
340             }
341              
342             sub _set_implied_name {
343 83     83   169 my $self = shift;
344 83         190 my ( $item, $element ) = @_;
345              
346 83         222 my $types = $item->types;
347              
348 83 100 100     1359 return if $item->has_properties || $item->has_children;
349              
350 59         6428 my $xpath;
351             my $name;
352 59         0 my $kid;
353 59         95 my $accept_if_empty = 1; # If true, then null-string names are okay.
354 59 100 100     175 if ( $element->tag =~ /^(img|area)$/ ) {
    100 100        
    100          
    100          
    100          
    100          
355 8         97 $xpath = './@alt';
356             }
357             elsif ( $element->tag eq 'abbr' ) {
358 1         21 $xpath = './@title';
359             }
360             elsif (
361             ( $kid = $self->_non_h_unique_child( $element, 'img' ) )
362             || ( $kid = $self->_non_h_unique_child( $element, 'area' ) )
363             ) {
364 7         13 $xpath = './@alt';
365 7         12 $accept_if_empty = 0;
366             }
367             elsif ( $kid = $self->_non_h_unique_child( $element, 'abbr' ) ) {
368 1         2 $xpath = './@title';
369 1         4 $accept_if_empty = 0;
370             }
371             elsif (
372             ( $kid = $self->_non_h_unique_grandchild( $element, 'img' ) )
373             || ( $kid = $self->_non_h_unique_grandchild( $element, 'area' ) )
374             ) {
375 3         7 $xpath = './@alt';
376 3         7 $accept_if_empty = 0;
377             }
378             elsif ( $kid = $self->_non_h_unique_grandchild( $element, 'abbr' ) ) {
379 1         3 $xpath = './@title';
380 1         3 $accept_if_empty = 0;
381             }
382              
383 59   66     190 my $foo = $kid || $element;
384              
385 59 100       124 if ( $xpath ) {
386 21   66     65 my $element_to_check = $kid || $element;
387 21         88 my $value = $element_to_check->findvalue( $xpath );
388 21 100 66     8507 if ( ( $value ne q{} ) || $accept_if_empty ) {
389 19         39 $name = $value;
390             }
391             }
392              
393 59 100       134 unless ( defined $name ) {
394 40         107 $name = _trim( $element->as_text );
395             }
396              
397 59 50       212 if ( length $name > 0 ) {
398 59         193 $item->add_property( 'p-name', $name );
399             }
400              
401             }
402              
403             sub _set_implied_photo {
404 126     126   224 my $self = shift;
405 126         239 my ( $item, $element ) = @_;
406              
407 126         291 my $xpath;
408             my $url;
409 126         0 my $kid;
410              
411 126 100       338 if ( $element->tag eq 'img' ) {
    100          
    100          
    100          
    100          
    100          
412 5         43 $xpath = './@src';
413             }
414             elsif ( $element->tag eq 'object' ) {
415 1         13 $xpath = './@data';
416             }
417             elsif ( $kid = $self->_non_h_unique_child( $element, 'img' ) ) {
418 4         8 $xpath = './@src';
419 4         8 $element = $kid;
420             }
421             elsif ( $kid = $self->_non_h_unique_child( $element, 'object' ) ) {
422 2         13 $xpath = './@data';
423 2         5 $element = $kid;
424             }
425             elsif ( $kid = $self->_non_h_unique_grandchild( $element, 'img' ) ) {
426 3         6 $xpath = './@src';
427 3         7 $element = $kid;
428             }
429             elsif ( $kid = $self->_non_h_unique_grandchild( $element, 'object' ) ) {
430 1         4 $xpath = './@data';
431 1         11 $element = $kid;
432             }
433              
434 126 100       288 if ( $xpath ) {
435 16         52 $url = $element->findvalue( $xpath );
436             }
437              
438 126 100       6252 if ( defined $url ) {
439 16         368 $url = URI->new_abs( $url, $self->url_context )->as_string;
440 16         4882 $item->add_property( 'u-photo', $url );
441             }
442              
443             }
444              
445             sub _set_implied_url {
446 115     115   171 my $self = shift;
447 115         217 my ( $item, $element ) = @_;
448              
449 115         271 my $xpath;
450             my $url;
451              
452 115         0 my $kid;
453 115 100 100     295 if ( $element->tag =~ /^(a|area)$/ ) {
    100 66        
      100        
454 23         227 $xpath = './@href';
455             }
456             elsif (
457             ( $kid = $self->_non_h_unique_child( $element, 'a' ) )
458             || ( $kid = $self->_non_h_unique_child( $element, 'area' ) )
459             || ( $kid = $self->_non_h_unique_grandchild( $element, 'a' ) )
460             || ( $kid = $self->_non_h_unique_grandchild( $element, 'area' ) )
461             ) {
462 10         19 $xpath = './@href';
463 10         19 $element = $kid;
464             }
465              
466 115 100       268 if ( $xpath ) {
467 33         115 $url = $element->findvalue( $xpath );
468             }
469              
470 115 100       12084 if ( defined $url ) {
471 33         741 $url = URI->new_abs( $url, $self->url_context )->as_string;
472 33         8427 $item->add_property( 'u-url', $url );
473             }
474              
475             }
476              
477             sub _non_h_unique_child {
478 642     642   3540 my $self = shift;
479 642         1139 my ( $element, $tag ) = @_;
480              
481 642 100       1316 my @children = grep { (ref $_) && $_->tag eq $tag } $element->content_list;
  1864         10095  
482              
483 642 100       2483 if ( @children == 1 ) {
484 38         107 my $mf2_attrs = $self->_tease_out_mf2_attrs( $children[0] );
485 38 100       136 if (not ( $mf2_attrs->{h}->[0] ) ) {
486 32         177 return $children[0];
487             }
488             }
489              
490 610         2323 return;
491             }
492              
493             sub _non_h_unique_grandchild {
494 512     512   767 my $self = shift;
495 512         861 my ( $element, $tag ) = @_;
496              
497 512         1023 my @children = grep { ref $_ } $element->content_list;
  1675         4718  
498              
499 512 100       1150 if ( @children == 1 ) {
500 117         258 my $mf2_attrs = $self->_tease_out_mf2_attrs( $children[0] );
501 117 100       301 if (not ( $mf2_attrs->{h}->[0] ) ) {
502 89         200 return $self->_non_h_unique_child( $children[0], $tag );
503             }
504             }
505              
506 423         1686 return;
507             }
508              
509             sub _clear {
510 74     74   159 my $self = shift;
511              
512 74         2074 $self->_clear_url_context;
513             }
514              
515             sub _seek_value_class_pattern {
516 348     348   508 my $self = shift;
517              
518 348         720 my ( $element, $vcp_fragments_ref ) = @_;
519              
520 348   100     1391 $vcp_fragments_ref ||= [];
521              
522 348         964 my $class = $element->attr( 'class' );
523 348 100 100     4642 if ( $class && $class =~ /\bvalue(-title)?\b/ ) {
524 57 100 100     193 if ( $1 ) {
    100          
525 6         17 push @$vcp_fragments_ref, $element->attr( 'title' );
526             }
527             elsif ( ( $element->tag =~ /^(del|ins|time)$/ ) && defined( $element->attr('datetime'))) {
528 20         399 push @$vcp_fragments_ref, $element->attr('datetime');
529             }
530             else {
531 31         361 my $html;
532 31         79 for my $content_piece ( $element->content_list ) {
533 31 50       176 if ( ref $content_piece ) {
534 0         0 $html .= $content_piece->as_HTML;
535             }
536             else {
537 31         81 $html .= $content_piece;
538             }
539             }
540 31         76 push @$vcp_fragments_ref, $html;
541             }
542             }
543             else {
544 291         691 for my $child_element ( grep { ref $_ } $element->content_list ) {
  417         2091  
545 84         281 $self->_seek_value_class_pattern(
546             $child_element, $vcp_fragments_ref
547             );
548             }
549             }
550              
551 348         1001 return $vcp_fragments_ref;
552             }
553              
554             sub _trim {
555 231     231   124279 my ($string) = @_;
556 231         862 $string =~ s/^\s+//;
557 231         943 $string =~ s/\s+$//;
558 231         677 return $string;
559             }
560              
561             sub _format_datetime {
562 22     22   50 my ($self, $dt_string, $current_item) = @_;
563              
564 22         36 my $dt;
565              
566             # Knock off leading/trailing whitespace.
567 22         44 $dt_string = _trim($dt_string);
568              
569 22         53 $dt_string =~ s/t/T/;
570              
571             # Note presence of AM/PM, but toss it out of the string.
572 22         126 $dt_string =~ s/((?:a|p)\.?m\.?)//i;
573 22   50     82 my $am_or_pm = $1 || '';
574              
575             # Store the provided TZ offset.
576 22         88 my ($provided_offset) = $dt_string =~ /([\-\+Z](?:\d\d:?\d\d)?)$/;
577 22   100     86 $provided_offset ||= '';
578              
579             # Reformat HHMM offset as HH:MM.
580 22         100 $dt_string =~ s/(-|\+)(\d\d)(\d\d)/$1$2:$3/;
581              
582             # Store the provided seconds.
583 22         74 my ($seconds) = $dt_string =~ /\d\d:\d\d:(\d\d)/;
584 22 100       68 $seconds = '' unless defined $seconds;
585              
586             # Insert :00 seconds on time when paired with a TZ offset.
587 22         83 $dt_string =~ s/T(\d\d:\d\d)([\-\+Z])/T$1:00$2/;
588 22         49 $dt_string =~ s/^(\d\d:\d\d)([\-\+Z])/$1:00$2/;
589              
590             # Zero-pad hours when only a single-digit hour appears.
591 22         68 $dt_string =~ s/T(\d)$/T0$1/;
592 22         58 $dt_string =~ s/T(\d):/T0$1:/;
593              
594             # Insert :00 minutes on time when only an hour is listed.
595 22         101 $dt_string =~ s/T(\d\d)$/T$1:00/;
596              
597             # Treat a space separator between date & time as a 'T'.
598 22         43 $dt_string =~ s/ /T/;
599              
600             # If this is a time with no date, try to apply a previously-seen
601             # date to it.
602 22         31 my $date_is_defined = 1;
603 22 100       66 if ( $dt_string =~ /^\d\d:/ ) {
604 1 50       24 if ( my $previous_dt = $current_item->last_seen_date ) {
605 1         23 $dt_string = $previous_dt->ymd . "T$dt_string";
606             }
607             else {
608 0         0 $date_is_defined = 0;
609 0         0 carp "Encountered a value-class datetime with only a time, "
610             . "no date, and no date defined earlier. Results may "
611             . "not be what you expect. (Data: $dt_string)";
612             }
613             }
614              
615 22         50 eval {
616 22         127 $dt = DateTime::Format::ISO8601->new
617             ->parse_datetime( $dt_string );
618             };
619              
620 22 50       14931 return if $@;
621              
622 22 50       56 if ($date_is_defined) {
623 22         546 $current_item->last_seen_date( $dt );
624             }
625              
626 22 100       852 if ($am_or_pm =~ /^[pP]/) {
627             # There was a 'pm' specified, so add 12 hours.
628 7         26 $dt->add( hours => 12 );
629             }
630              
631 22         7080 my $format;
632 22 100 66     171 if ( ($dt_string =~ /-/) && ($dt_string =~ /[ T]/) ) {
    50          
633 21         32 my $offset;
634 21 100       66 if ($provided_offset eq 'Z') {
    100          
635 2         5 $offset = 'Z';
636             }
637             elsif ($provided_offset) {
638 7         11 $offset = '%z';
639             }
640             else {
641 12         25 $offset = '';
642             }
643 21 100       68 $seconds = ":$seconds" if length $seconds;
644 21         62 $format = "%Y-%m-%d %H:%M$seconds$offset";
645             }
646             elsif ( $dt_string =~ /-/ ) {
647 1         3 $format = '%Y-%m-%d';
648             }
649              
650 22         79 return $dt->strftime( $format );
651             }
652              
653             sub _parse_property_value {
654 170     170   325 my ( $self, $element ) = @_;
655              
656 170         259 my $value;
657              
658 170         367 my $vcp_fragments_ref =
659             $self->_seek_value_class_pattern( $element );
660 170 100       656 if ( @$vcp_fragments_ref ) {
    100          
    100          
661 11         31 $value = join q{}, @$vcp_fragments_ref;
662             }
663             elsif ( my $alt = $element->findvalue( './@title|@value|@alt' ) ) {
664 9         8602 $value = $alt;
665             }
666             elsif ( my $text = _trim( decode_entities($element->as_text) ) ) {
667 148         246 $value = $text;
668             }
669              
670 170         447 return $value;
671             }
672              
673             sub _add_element_rels_to_mf2_document {
674 898     898   1613 my ( $self, $element, $document ) = @_;
675              
676 898 100       2217 return unless $element->tag =~ /^(a|link)$/;
677              
678 99         1038 my $rel = $element->attr( 'rel' );
679 99 100       1175 return unless defined $rel;
680              
681 42         87 my $href = $element->attr( 'href' );
682 42         1195 my $url = URI->new_abs( $href, $self->url_context)->as_string;
683              
684 42         5246 my @rels = split /\s+/, $rel;
685 42         151 for my $rel ( @rels ) {
686 48         154 $document->add_rel( $rel, $url );
687             }
688              
689 42         80 my $rel_url_value = {};
690 42         90 foreach (qw( hreflang media title type ) ) {
691 168 50       306 next if defined $rel_url_value->{ $_ };
692 168         349 my $value = $element->attr( $_ );
693 168 100       1761 if ( defined $value ) {
694 6         17 $rel_url_value->{ $_ } = $value;
695             }
696             }
697 42         109 my $text = ($element->as_text);
698 42 50       1053 if ( defined $text ) {
699 42         95 $rel_url_value->{ text } = $text;
700             }
701              
702 42         82 $rel_url_value->{ rels } = \@rels;
703              
704 42         114 $document->add_rel_url( $url, $rel_url_value );
705              
706             }
707              
708             1;
709              
710             =pod
711              
712             =head1 NAME
713              
714             Web::Microformats2::Parser - Read Microformats2 information from HTML
715              
716             =head1 DESCRIPTION
717              
718             An object of this class represents a Microformats2 parser.
719              
720             See L<Web::Microformats2> for further context and purpose.
721              
722             =head1 METHODS
723              
724             =head2 Class Methods
725              
726             =head3 new
727              
728             $parser = Web::Microformats2::Parser->new;
729              
730             Returns a parser object.
731              
732             =head2 Object Methods
733              
734             =head3 parse
735              
736             $doc = $parser->parse( $html, %args );
737              
738             Pass in a string containing HTML which itself contains Microformats2
739             metadata, and receive a L<Web::Microformats2::Document> object in return.
740              
741             The optional args hash recognizes the following keys:
742              
743             =over
744              
745             =item url_context
746              
747             A L<URI> object or URI-shaped string that will be used as a context for
748             transforming all relative URL properties encountered within MF2 tags
749             into absolute URLs.
750              
751             The default value is C<http://example.com>, so you'll probably want to
752             set this to something more interesting, such as the absolute URL of the
753             HTML that we are parsing.
754              
755             =back
756              
757             =head1 AUTHOR
758              
759             Jason McIntosh (jmac@jmac.org)
760              
761             =head1 COPYRIGHT AND LICENSE
762              
763             This software is Copyright (c) 2018 by Jason McIntosh.
764              
765             This is free software, licensed under:
766              
767             The MIT (X11) License