File Coverage

blib/lib/HTML/Microdata.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package HTML::Microdata;
2              
3 2     2   25985 use strict;
  2         5  
  2         66  
4 2     2   11 use warnings;
  2         3  
  2         181  
5              
6 2     2   2708 use HTML::TreeBuilder::LibXML;
  0            
  0            
7             use Hash::MultiValue;
8             use Scalar::Util qw(refaddr);
9             use JSON;
10             use URI;
11              
12             our $VERSION = '0.04';
13              
14             sub new {
15             my ($class, %args) = @_;
16             bless {
17             items => [],
18             base => $args{base} ? URI->new($args{base}) : undef,
19             }, $class;
20             }
21              
22             sub extract {
23             my ($class, $content, %opts) = @_;
24             my $self = $class->new(%opts);
25             $self->_parse($content);
26             $self
27             }
28              
29             sub as_json {
30             my ($self) = @_;
31             encode_json +{
32             items => $self->{items},
33             };
34             }
35              
36             sub items {
37             my ($self) = @_;
38             $self->{items};
39             }
40              
41             sub _parse {
42             my ($self, $content) = @_;
43              
44             my $items = {};
45             my $tree = HTML::TreeBuilder::LibXML->new_from_content($content);
46             my $scopes = $tree->findnodes('//*[@itemscope]');
47             my $number = 0;
48              
49             for my $scope (@$scopes) {
50             my $type = $scope->attr('itemtype');
51             my $id = $scope->attr('itemid');
52              
53             unless ($scope->id) {
54             $scope->id($number++);
55             }
56              
57             my $item = {
58             ($id ? (id => $id) : ()),
59             ($type ? (type => $type) : ()),
60             properties => Hash::MultiValue->new,
61             };
62              
63             $items->{ $scope->id } = $item;
64              
65             unless ($scope->attr('itemprop')) {
66             # This is top level item
67             push @{ $self->{items} }, $item;
68             }
69             }
70              
71             for my $scope (@$scopes) {
72             if (my $refs = $scope->attr('itemref')) {
73             my $ids = [ split /\s+/, $refs ];
74             for my $id (@$ids) {
75             my $props = $tree->findnodes('//*[@id="' . $id . '"]/descendant-or-self::*[@itemprop]');
76             for my $prop (@$props) {
77             my $name = $prop->attr('itemprop');
78             my $value = $self->extract_value($prop, items => $items);
79             $items->{ $scope->id }->{properties}->add($name => $value);
80             $prop->delete;
81             }
82             }
83             }
84             }
85              
86             my $props = $tree->findnodes('//*[@itemscope]/descendant-or-self::*[@itemprop]');
87             for my $prop (@$props) {
88             my $value = $self->extract_value($prop, items => $items);
89             my $scope = $prop->findnodes('./ancestor::*[@itemscope]')->[-1];
90             for my $name (split /\s+/, $prop->attr('itemprop')) {
91             $items->{ $scope->id }->{properties}->add($name => $value);
92             }
93             }
94              
95             for my $key (keys %$items) {
96             my $item = $items->{$key};
97             $item->{properties} = $item->{properties}->multi;
98             }
99              
100             }
101              
102             sub absolute {
103             my ($self, $uri) = @_;
104             if (defined $uri) {
105             if ($self->{base}) {
106             URI->new_abs($uri, $self->{base}).q();
107             } else {
108             $uri;
109             }
110             } else {
111             "";
112             }
113             }
114              
115             sub extract_value {
116             my ($self, $prop, %opts) = @_;
117              
118             my $value;
119             if (defined $prop->attr('itemscope')) {
120             # XXX : inifinite loop
121             $value = $opts{items}->{ $prop->id };
122             } elsif ($prop->tag eq 'meta') {
123             $value = $prop->attr('content');
124             } elsif ($prop->tag =~ m{^audio|embed|iframe|img|source|video|track$}) {
125             $value = $self->absolute($prop->attr('src'));
126             } elsif ($prop->tag =~ m{^a|area|link$}) {
127             $value = $self->absolute($prop->attr('href'));
128             } elsif ($prop->tag eq 'object') {
129             $value = $self->absolute($prop->attr('data'));
130             } elsif ($prop->tag eq 'data') {
131             $value = $prop->attr('value');
132             } elsif ($prop->tag eq 'time' && $prop->attr('datetime')) {
133             $value = $prop->attr('datetime');
134             } else {
135             $value = $prop->findvalue('normalize-space(.)');
136             }
137              
138             $value;
139             }
140              
141             1;
142             __END__