File Coverage

blib/lib/Role/Markup/XML.pm
Criterion Covered Total %
statement 15 17 88.2
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 21 23 91.3


line stmt bran cond sub pod time code
1             package Role::Markup::XML;
2              
3 1     1   45457 use 5.010;
  1         3  
4 1     1   4 use strict;
  1         3  
  1         21  
5 1     1   4 use warnings FATAL => 'all';
  1         5  
  1         38  
6              
7 1     1   344 use Moo::Role;
  1         12054  
  1         5  
8 1     1   588 use namespace::autoclean;
  1         9411  
  1         4  
9              
10 1     1   123 use XML::LibXML ();
  0            
  0            
11             use Scalar::Util ();
12             use Carp ();
13              
14             use constant XHTMLNS => 'http://www.w3.org/1999/xhtml';
15              
16             # XXX this is a shitty qname regex: no unicode but whatever
17             #use constant QNAME_RE =>
18             # qr/^(?:([A-Za-z][0-9A-Za-z_-]*):)?([A-Za-z][0-9A-Za-z_-]*)$/;
19              
20             use constant NCNAME_PAT => do {
21             my $ns = '_A-Za-z\N{U+C0}-\N{U+D6}\N{U+D8}-\N{U+F6}\N{U+F8}-\N{U+2FF}' .
22             '\N{U+370}-\N{U+37D}\N{U+37F}-\N{U+1FFF}\N{U+200C}-\N{U+200D}' .
23             '\N{U+2070}-\N{U+218F}\N{U+2C00}-\N{U+2FEF}\N{U+3001}-\N{U+D7FF}' .
24             '\N{U+F900}-\N{U+FDCF}\N{U+FDF0}-\N{U+FFFD}' .
25             '\N{U+10000}-\N{U+EFFFF}';
26             my $nc = '0-9\N{U+B7}\N{U+300}-\N{U+36F}\N{U+203F}-\N{U+2040}-';
27             sprintf '[%s][%s%s]*', $ns, $ns, $nc;
28             };
29              
30             use constant NCNAME_RE => do {
31             my $nc = NCNAME_PAT;
32             qr/^($nc)$/o;
33             };
34              
35             use constant QNAME_RE => do {
36             my $nc = NCNAME_PAT;
37             qr/^(?:($nc):)?($nc)$/o;
38             };
39              
40             #STDERR->binmode('utf8');
41             #warn NCNAME_RE;
42              
43              
44             has _ATTRS => (
45             is => 'ro',
46             isa => sub { Carp::croak('Input must be a HASH reference')
47             unless ref $_[0] eq 'HASH' },
48             default => sub { { } },
49             );
50              
51             =head1 NAME
52              
53             Role::Markup::XML - Moo(se) role for bolt-on lazy XML markup
54              
55             =head1 VERSION
56              
57             Version 0.05
58              
59             =cut
60              
61             our $VERSION = '0.05';
62              
63             =head1 SYNOPSIS
64              
65             package My::MarkupEnabled;
66              
67             use Moo; # or Moose, or something compatible
68             with 'Role::Markup::XML'; # ...and this of course
69              
70             # write some other code...
71              
72             sub something_useful {
73             my $self = shift;
74              
75             # put your XML-generating data structure here
76             my %spec = (
77             -name => 'my:foo', # element name
78             -content => { -name => 'my:bar' }, # element content
79             hurr => 'durr', # attribute
80             'my:derp' => 'lulz', # namespaced attribute
81             'xmlns:my' => 'urn:x-bogus:foo', # namespaces go inline
82             );
83              
84             # create a document object to hang on to
85             my $doc = $self->_DOC;
86              
87             # returns the last node generated, which is
88             my $stub = $self->_XML(
89             doc => $doc,
90             spec => \%spec,
91             );
92              
93             my @contents = (
94             # imagine a bunch of things in here
95             );
96              
97             # since these nodes will be appended to $stub, we aren't
98             # interested in the output this time
99             $self->_XML(
100             parent => $stub, # owner document is derived
101             spec => \@contents, # also accepts ARRAY refs
102             args => $self->cb_args, # some useful state data
103             );
104              
105             # the rest of the ops come from XML::LibXML
106             return $doc->toString(1);
107             }
108              
109             =head1 DESCRIPTION
110              
111             This is indeed yet another module for lazy XML markup generation. It
112             exists because it is different:
113              
114             =over 4
115              
116             =item
117              
118             It converses primarily in reusable, inspectable, and most importantly,
119             I Perl data structures,
120              
121             =item
122              
123             It also ingests existing L nodes,
124              
125             =item
126              
127             It enables you to generate markup I, rather than all at
128             once,
129              
130             =item
131              
132             It Does the Right ThingE<0x2122> around a bunch of otherwise tedious
133             boilerplate operations, such as namespaces, XHTML, or flattening
134             token lists in attributes,
135              
136             =item
137              
138             It has a callback infrastructure to help you create modular templates,
139             or otherwise override behaviour you don't like,
140              
141             =item
142              
143             It is implemented as a Role, to be more conducive to modern Perl
144             development.
145              
146             =back
147              
148             I began by using L. It is pretty good,
149             definitely preferable to typing out reams of L DOM-like
150             API any time I wanted to make some (guaranteed well-formed) XML. I
151             even submitted a patch to it to make it better. Nevertheless, I have
152             reservations about the general approach to terse markup-generating
153             libraries, in particular about the profligate use of anonymous
154             subroutines. (You also see this in
155             L for Python,
156             L
157             for Ruby, etc.)
158              
159             The main issue is that these languages aren't Lisp: it costs something
160             at runtime to gin up a stack of nested anonymous subroutines, run them
161             once, and then immediately throw them away. It likewise costs in
162             legibility to have to write a bunch of imperative code to do what is
163             essentially data declaration. It also costs in sanity to have to write
164             function-generating-function-generating functions just to get the mess
165             under control. What you get for your trouble is an interim product
166             that is impossible to inspect or manipulate. This ostensibly
167             time-saving pattern quickly hits a wall in both development, and at
168             runtime.
169              
170             The answer? Use (in this case) Perl's elementary data structures to
171             convey the requisite information: data structures which can be built
172             up from bits and pieces, referenced multiple times, sliced, diced,
173             spliced, frozen, thawed, inspected, and otherwise operated on by
174             ordinary Perl routines. Provide mix-and-match capability with vanilla
175             L, callbacks, and make the whole thing an unobtrusive
176             mix-in that you can bolt onto your existing code.
177              
178             =head1 METHODS
179              
180             Methods in this module are named such as to stay out of the way of
181             I module's interface.
182              
183             =head2 _DOC [$VERSION,] [$ENCODING]
184              
185             Generate a document node. Shorthand for L.
186              
187             =cut
188              
189             sub _DOC {
190             my (undef, $version, $encoding) = @_;
191             $version ||= '1.0';
192             $encoding ||= 'utf-8';
193              
194             XML::LibXML::Document->new($version, $encoding);
195             }
196              
197             =head2 _ELEM $TAG [, $DOC, \%NSMAP ]
198              
199             Generate a single XML element. Generates a new document unless C<$DOC>
200             is specified. Defaults to XHTML if no namespace map is provided.
201              
202             =cut
203              
204             sub _ELEM {
205             my ($self, $tag, $doc, $nsmap) = @_;
206             my ($prefix, $local) = ($tag =~ QNAME_RE);
207             $prefix ||= '';
208             $doc ||= $self->_DOC;
209              
210             my $ns = $nsmap && $nsmap->{$prefix} ? $nsmap->{$prefix} : XHTMLNS;
211              
212             my $elem = $doc->createElementNS($ns, $tag);
213             for my $k (sort keys %$nsmap) {
214             $elem->setNamespace($nsmap->{$k}, $k, $prefix eq $k);
215             }
216              
217             # add boilerplate attributes
218             if (my $a = $self->_ATTRS->{$tag}) {
219             map { $elem->setAttribute($_ => $a->{$_}) } keys %$a;
220             }
221              
222             $elem;
223             }
224              
225             =head2 _XML $SPEC [, $PARENT, $DOC, $ARGS | @ARGS ] | %PARAMS
226              
227             Generate an XML tree according to the L
228             format|/Specification Format>. Returns the I by
229             the process. Parameters are as follows:
230              
231             =over 4
232              
233             =item spec
234              
235             The node specification. Strictly speaking this is optional, but there
236             isn't much of a point of running this method if there is no spec to
237             run it over.
238              
239             =item doc
240              
241             The L object intended to own the
242             contents. Optional, however it is often desirable to supply a document
243             object along with the initial call to this method, so as not to have
244             to fish it out later.
245              
246             =item parent
247              
248             The L (or, redundantly, Document) object which
249             is intended to be the parent node of the spec. Optional.
250              
251             =item args
252              
253             An C reference of arguments to be passed into C
254             references embedded in the spec. Optional.
255              
256             =back
257              
258             =head3 Specification Format
259              
260             The building blocks of the spec are, unsurprisingly, C and
261             C references. The former correspond to elements and other
262             things, while the latter correspond to lists thereof. Literals become
263             text nodes, and blessed objects will be treated like strings, so it
264             helps if they have a string L. C references may be
265             used just about anywhere, and will be dereferenced recursively using
266             the supplied L until there is nothing left to dereference. It
267             is up to I to keep these data structures free of cycles.
268              
269             =over 4
270              
271             =item Elements
272              
273             Special keys designate the name and content of an element spec. These
274             are, unimaginitively, C<-name> and C<-content>. They work like so:
275              
276             { -name => 'body', -content => 'hurr' }
277              
278             # produces hurr
279              
280             Note that C<-content> can take any primitive: literal, C,
281             C or C reference, L object, etc.
282              
283             =item Attributes
284              
285             Any key is not C<-name> or C<-content> will be interpreted as an attribute.
286              
287             { -name => 'body', -content => 'hurr', class => 'lolwut' }
288              
289             # produces hurr
290              
291             When references are values of attributes, they are flattened into strings:
292              
293             { -name => 'body', -content => 'hurr', class => [qw(one two three)] }
294              
295             # produces hurr
296              
297             =item Namespaces
298              
299             If there is a colon in either the C<-name> key value or any of the
300             attribute keys, the processor will expect a namespace that corresponds
301             to that prefix. These are specified exactly as one would with ordinary
302             XML, with the use of an C attribute>. (Prefix-free C
303             attributes likewise work as expected.)
304              
305             { -name => 'svg',
306             xmlns => 'http://www.w3.org/2000/svg',
307             'xmlns:xlink' => 'http://www.w3.org/1999/xlink',
308             -content => [
309             { -name => 'a', 'xlink:href' => 'http://some.host/' },
310             ],
311             }
312              
313             # produces:
314             #
315             # xmlns:xlink="http://www.w3.org/1999/xlink">
316             #
317             #
318              
319             =item Other Nodes
320              
321             =over 4
322              
323             =item C<-pi>
324              
325             Processing instructions are designated by the special key C<-pi> and
326             accept arbitrary pseudo-attributes:
327              
328             { -pi => 'xml-stylesheet', type => 'text/xsl', href => '/my.xsl' }
329              
330             # produces
331              
332             =item C<-doctype>
333              
334             Document type declarations are designated by the special key
335             C<-doctype> and accept values for the keys C and C:
336              
337             { -doctype => 'html' }
338              
339             # produces
340              
341             =item C<-comment>
342              
343             Comments are designated by the special key C<-comment> and whatever is
344             in the value of that key:
345              
346             { -comment => 'hey you guyyyys' }
347              
348             # produces
349              
350             =back
351              
352             =item Callbacks
353              
354             Just about any part of a markup spec can be replaced by a C
355             reference, which can return any single value, including another
356             C reference. These are called in the context of C<$self>, i.e.,
357             as if they were a method of the object that does the role. The
358             L in the original method call form the subsequent input:
359              
360             sub callback {
361             my ($self, @args) = @_;
362              
363             my %node = (-name => 'section', id => $self->generate_id);
364              
365             # ...do things to %node, presumably involving @args...
366              
367             return \%node;
368             }
369              
370             sub make_xml {
371             my $self = shift;
372              
373             my $doc = $self->_DOC;
374             $self->_XML(
375             doc => $doc,
376             spec => { -name => 'p', -content => \&callback },
377             );
378              
379             return $doc;
380             }
381              
382             C references can appear in attribute values as well.
383              
384             =back
385              
386             =cut
387              
388             sub _flatten {
389             my ($self, $spec, $args) = @_;
390             if (my $ref = ref $spec) {
391             if ($ref eq 'ARRAY') {
392             return join ' ', grep { defined $_ }
393             map { $self->_flatten($_, $args) } @$spec;
394             }
395             elsif ($ref eq 'HASH') {
396             return join ' ',
397             map { join ': ', $_, $self->_flatten($spec->{$_}, $args) }
398             grep { defined $spec->{$_} } sort keys %$spec;
399             }
400             elsif ($ref eq 'CODE') {
401             return $self->_flatten($spec->($self, @$args), $args);
402             }
403             else {
404             return "$spec";
405             }
406             }
407             else {
408             return $spec;
409             }
410             }
411              
412             # figure out if the child should be some kind of table tag based on the parent
413             sub _table_tag {
414             my $parent = shift or return;
415              
416             my %is_tr = (thead => 1, tfoot => 1, tbody => 1);
417             my %is_th = (thead => 1, tfoot => 1);
418              
419             if ($parent->nodeType == 1) {
420             my $pln = $parent->localname;
421              
422             return 'tr' if $is_tr{$pln};
423              
424             if ($pln eq 'tr') {
425             my $gp = $parent->parentNode;
426             if ($gp and $gp->nodeType == 1) {
427             return $is_th{$gp->localname} ? 'th' : 'td';
428             }
429             }
430             }
431              
432             return;
433             }
434              
435             sub _attach {
436             my ($node, $parent) = @_;
437              
438             if ($node && $parent) {
439             if ($parent->nodeType == 9 and $node->nodeType == 1) {
440             $parent->setDocumentElement($node);
441             }
442             else {
443             $parent->appendChild($node);
444             }
445             }
446             $node;
447             }
448              
449             sub _XML {
450             my $self = shift;
451             my %p;
452             if (ref $_[0]) {
453             $p{spec} = $_[0];
454             @p{qw(parent doc)} = @_[1,2];
455             if (defined $_[3] and ref $_[3] eq 'ARRAY') {
456             $p{args} = $_[3];
457             }
458             else {
459             $p{args} = [@_[3..$#_]];
460             }
461             }
462             else {
463             %p = @_;
464             }
465              
466             $p{args} ||= [];
467             $p{doc} ||= $p{parent} && $p{parent}->ownerDocument
468             ? $p{parent}->ownerDocument : $self->_DOC;
469             $p{parent} ||= $p{doc}; # this might be problematic
470              
471             my $node;
472              
473             my $ref = ref($p{spec}) || '';
474             if ($ref eq 'ARRAY') {
475             my @out = map {
476             $self->_XML(spec => $_, parent => $p{parent},
477             doc => $p{doc}, args => $p{args}) } @{$p{spec}};
478             if (@out) {
479             return wantarray ? @out : $out[-1];
480             }
481             return $p{parent};
482             }
483             elsif ($ref eq 'CODE') {
484             return $self->_XML(spec => $p{spec}->($self, @{$p{args}}),
485             parent => $p{parent},
486             doc => $p{doc},
487             args => $p{args});
488             }
489             elsif ($ref eq 'HASH') {
490             # copy the spec so we don't screw it up
491             my %spec = %{$p{spec}};
492              
493             if (my $c = $spec{'-comment'}) {
494             $node = $p{doc}->createComment($self->_flatten($c, @{$p{args}}));
495             _attach($node, $p{parent});
496             return $node;
497             }
498             if (my $target = delete $spec{'-pi'}) {
499             # take -content over content
500             my $content = defined $spec{'-content'} ?
501             delete $spec{'-content'} : delete $spec{content};
502             my $data = join ' ', map {
503             sprintf q{%s="%s"}, $_, $self->_flatten($spec{$_}, @{$p{args}})
504             } sort keys %spec;
505              
506             $data .= ' ' .
507             $self->_flatten($content, @{$p{args}}) if defined $content;
508              
509             $node = $p{doc}->createProcessingInstruction($target, $data);
510             _attach($node, $p{parent});
511             return $node;
512             }
513             elsif (my $dtd = $spec{'-doctype'} || $spec{'-dtd'}) {
514             # in XML::LibXML::LazyBuilder i wrote that there is some
515             # XS issue and these values have to be explicitly passed
516             # in as undef.
517             my $public = $spec{public};
518             my $system = $spec{system};
519             $node = $p{doc}->createExternalSubset
520             ($dtd, $public || undef, $system || undef);
521             _attach($node, $p{doc});
522             return $node;
523             }
524             else {
525             # check for specified tag
526             my $tag = delete $spec{'-name'};
527              
528             my ($prefix, $local);
529             if (defined $tag) {
530             ($prefix, $local) = ($tag =~ QNAME_RE);
531             Carp::croak("Cannot make use of tag $tag")
532             unless defined $local;
533             }
534             $prefix ||= '';
535              
536             # detect appropriate table tag
537             unless ($tag ||= _table_tag($p{parent})) {
538             my $is_head = $p{parent}->nodeType == 1 &&
539             $p{parent}->localname eq 'head';
540             # detect tag
541             if (defined $spec{src}) {
542             $tag = 'img';
543             }
544             elsif (defined $spec{href}) {
545             $tag = $is_head ? 'link' : 'a';
546             }
547             else {
548             $tag = $is_head ? 'meta' : 'span';
549             }
550             }
551              
552             # okay generate the node
553             my %ns;
554              
555             if (my $nsuri = $p{parent}->lookupNamespaceURI($prefix)) {
556             $ns{$prefix} = $nsuri;
557             }
558              
559             for my $k (keys %spec) {
560             next unless $k =~ /^xmlns(?::(.*))/;
561             my $prefix = $1 || '';
562              
563             my $v = delete $spec{$k};
564             $v =~ s/^\s*(.*?)\s*$/$1/; # trim
565             $ns{$prefix} = $v;
566             }
567             $node = $self->_ELEM($tag, $p{doc}, \%ns);
568             _attach($node, $p{parent});
569              
570             # now do the attributes
571             for my $k (sort grep { $_ =~ QNAME_RE } keys %spec) {
572              
573             my $val = $self->_flatten($spec{$k}, $p{args});
574             $node->setAttribute($k => $val) if (defined $val);
575             }
576              
577             # special handler for explicit content
578             my $content = delete $spec{'-content'};
579             return $self->_XML(
580             spec => $content,
581             parent => $node,
582             doc => $p{doc},
583             args => $p{args}
584             ) if defined $content;
585             }
586             }
587             elsif (Scalar::Util::blessed($p{spec})
588             and $p{spec}->isa('XML::LibXML::Node')) {
589             $node = $p{spec}->cloneNode(1);
590             _attach($node, $p{parent});
591             }
592             else {
593             # spec is a text node, if defined
594             if (defined $p{spec}) {
595             $node = $p{doc}->createTextNode("$p{spec}");
596             _attach($node, $p{parent});
597             }
598             }
599              
600             $node;
601             }
602              
603             =head2 _XHTML | %PARAMS
604              
605             Generate an XHTML+RDFa stub. Return the CbodyE> and the
606             document when called in list context, otherwise return just the
607             CbodyE> in scalar context (which can be used in subsequent
608             calls to L).
609              
610             my ($body, $doc) = $self->_XHTML(%p);
611              
612             # or
613              
614             my $body = $self->_XHTML(%p);
615              
616             =head3 Parameters
617              
618             =over 4
619              
620             =item uri
621              
622             The C attribute of the CbaseE> element.
623              
624             =item ns
625              
626             A mapping of namespace prefixes to URIs, which by default will appear
627             as I XML namespaces I the C attribute.
628              
629             =item prefix
630              
631             Also a mapping of prefixes to URIs. If this is set rather than C,
632             then the XML namespaces will I be set. Conversely, if this
633             parameter is defined but false, then I the contents of C
634             will appear in the conventional C way.
635              
636             =item vocab
637              
638             This will specify a default C attribute in the
639             ChtmlE> element, like L.
640              
641             =item title
642              
643             This can either be a literal title string, or C reference, or
644             C reference assumed to encompass the whole CtitleE>
645             element, or an C reference where the first element is the title
646             and subsequent elements are predicates.
647              
648             =item link
649              
650             This can either be an C reference of ordinary markup specs, or
651             a C reference where the keys are the C attribute and the
652             values are one or more (via C ref) URIs. In the latter form the
653             following behaviour holds:
654              
655             =over 4
656              
657             =item
658              
659             Predicates are grouped by C, folded, and sorted alphabetically.
660              
661             =item
662              
663             ClinkE> elements are sorted first lexically by the sorted
664             C, then by sorted C, then by C.
665              
666             =item
667              
668             A special empty C<""> hash key can be used to pass in another similar
669             structure whose keys represent C, or reverse predicates.
670              
671             =item
672              
673             A special C<-about> key can be used to specify another C
674             reference where the keys are subjects and the values are similar
675             structures to the one described.
676              
677             =back
678              
679             {
680             # ordinary links
681             'rel:prop' => [qw(urn:x-target:1 urn:x-target:2)],
682              
683             # special case for reverse links
684             '' => { 'rev:prop' => 'urn:x-demo-subject:id' },
685              
686             # special case for alternate subject
687             -about => {
688             'urn:x-demo-subject:id' => { 'some:property' => 'urn:x-target' } },
689             }
690              
691             The C reference form is passed along as-is.
692              
693             =item meta
694              
695             Behaves similarly to the C parameter, with the following exceptions:
696              
697             =over 4
698              
699             =item
700              
701             No C<""> or C<-about> pseudo-keys, as they are meaningless for
702             literals.
703              
704             =item
705              
706             Literal values can be expressed as an C reference of the form
707             C<[$val, $lang, $type]> with either the second or third element
708             C. They may also be represented as a C reference where
709             the keys are the language (denoted by a leading C<@>) or datatype
710             (everything else), and the values are the literal values.
711              
712             =back
713              
714             {
715             'prop:id' => ['foo', [2.3, undef, 'xsd:decimal']],
716             'exotic' => { '@en' => ['yo dawg', 'derp'] }
717             }
718              
719             =item head
720              
721             This is an optional C reference of C<> elements that
722             are neither C<> nor C<> (or, if you want,
723             additional unmolested C<> and C<> elements).
724              
725             =item attr
726              
727             These attributes (including C<-content>) will be passed into the
728             C<> element.
729              
730             =item content
731              
732             This parameter enables us to isolate the C<> content without
733             additional attributes.
734              
735             Note that setting this parameter will cause the method to return the
736             innermost, last node that is specified, rather than the C<>.
737              
738             =item transform
739              
740             This is the URI of a (e.g. XSLT) transform which will be included in a
741             processing instruction if supplied.
742              
743             =item args
744              
745             Same as C in L.
746              
747             =back
748              
749             =cut
750              
751             sub _sort_links {
752             # first test is about
753             #warn Data::Dumper::Dumper(\@_);
754             my @a = map { defined $_->{about} ? $_->{about} : '' } @_;
755             my $t1 = $a[0] cmp $a[1];
756             return $t1 if $t1;
757              
758             # then rel
759             my @rl = map { defined $_->{rel} ? $_->{rel} : '' } @_;
760             my $t2 = $rl[0] cmp $rl[1];
761             return $t2 if $t2;
762              
763             # then rev
764             my @rv = map { defined $_->{rev} ? $_->{rev} : '' } @_;
765             my $t3 = $rv[0] cmp $rv[1];
766             return $t3 if $t3;
767              
768             # then finally href
769             my @h = map { defined $_->{href} ? $_->{href} : '' } @_;
770             return $h[0] cmp $h[1];
771             }
772              
773             sub _handle_links {
774             my ($links, $uri) = @_;
775             $links ||= [];
776             return @$links if ref $links eq 'ARRAY';
777             Carp::croak('links must be ARRAY or HASH ref') unless ref $links eq 'HASH';
778              
779             my %l = %$links;
780             my %r = %{delete $l{''} || {}};
781             my %s = %{delete $l{-about} || {}};
782              
783              
784             # merge subjects; blank subject is document
785             %{$s{''} ||= {}} = (%{$s{''} || {}}, %l);
786              
787             my (%types, %titles); # map URIs to types and titles
788              
789             # accumulate predicates into a hierarchical structure of S -> O -> P
790             my (%fwd, %rev);
791             for my $s (keys %s) {
792             for my $p (keys %{$s{$s}}) {
793             my @o = ref $s{$s}{$p} eq 'ARRAY' ? @{$s{$s}{$p}} : ($s{$s}{$p});
794             for my $o (@o) {
795             my ($href, $type, $title) = ref $o eq 'ARRAY' ? @$o : ($o);
796             # XXX do a better uri match
797             $href = '' if $uri and $href eq $uri;
798             # XXX this overwrites titles oh well suck one
799             $types{$href} = $type;
800             $titles{$href} = $title;
801              
802             # accumulate the predicates
803             my $x = $fwd{$s} ||= {};
804             my $y = $rev{$href} ||= {};
805             my $z = $x->{$href} ||= $y->{$s} ||= {};
806             $z->{$p}++;
807             }
808             }
809             }
810              
811             # now do reverse links
812             for my $p (keys %r) {
813             my @o = ref $r{$p} eq 'ARRAY' ? @{$r{$p}} : ($r{$p});
814              
815             for my $o (@o) {
816             # we skip type and title because the link is reversed
817             my ($s) = ref $o eq 'ARRAY' ? @$o : ($o);
818             # XXX do a better uri match
819             $s = '' if $uri and $s eq $uri;
820             my $x = $fwd{$s} ||= {};
821             my $y = $rev{''} ||= {};
822             my $z = $x->{''} ||= $y->{$s} ||= {};
823             $z->{$p}++;
824             }
825             }
826              
827             # now we have accumulated all the predicates and aimed all the
828             # triples in the forward direction. now to construct the list.
829              
830             my (%fout, %rout, @out);
831              
832             # begin by making sure typed links point forward
833             for my $o (keys %types) {
834             for my $s (keys %{$rev{$o}}) {
835             $fout{$s} ||= {};
836             $rout{$o} ||= {};
837              
838             my $x = $fout{$s}{$o};
839             unless ($x) {
840             $x = $fout{$s}{$o} = $rout{$o}{$s} = { href => $o,
841             type => $types{$o} };
842             $x->{about} = $s if $s ne '';
843             push @out, $x;
844             }
845              
846             $x->{type} = $types{$o};
847             }
848             }
849              
850             # now do the same with titles
851             for my $o (keys %titles) {
852             for my $s (keys %{$rev{$o}}) {
853             $fout{$s} ||= {};
854             $rout{$o} ||= {};
855              
856             my $x = $fout{$s}{$o};
857             unless ($x) {
858             $x = $fout{$s}{$o} = $rout{$o}{$s} = { href => $o,
859             title => $titles{$o} };
860             $x->{about} = $s if $s ne '';
861             push @out, $x;
862             }
863              
864             $x->{title} = $titles{$o};
865             }
866             }
867              
868             # now we make sure blank subjects always face forward
869             if ($fwd{''}) {
870             for my $o (sort keys %{$fwd{''}}) {
871             $fout{''} ||= {};
872             $rout{$o} ||= {};
873              
874             my $x = $fout{''}{$o};
875             unless ($x) {
876             $x = $fout{''}{$o} = $rout{$o}{''} = { href => $o };
877             push @out, $x;
878             }
879             }
880             }
881              
882             # now do forward predicates (this mapping is symmetric)
883             for my $s (sort keys %fwd) {
884             for my $o (sort keys %{$fwd{$s}}) {
885             $fout{$s} ||= {};
886             $rout{$o} ||= {};
887              
888             # collate up the predicates
889             my $p = join ' ', sort keys %{$fwd{$s}{$o}};
890              
891             # first try forward
892             my $x = $fout{$s}{$o};
893             if ($x) {
894             # set the link direction based on derp
895             $x->{$x->{href} eq $o ? 'rel' : 'rev'} = $p;
896             # make sure rel exists
897             $x->{rel} = '' unless defined $x->{rel};
898             }
899             else {
900             # then try backward
901             $x = $rout{$s}{$o};
902             if ($x) {
903             # do the same thing but the other way around
904             $x->{$x->{href} eq $o ? 'rel' : 'rev' } = $p;
905             # and make sure rel exists
906             $x->{rel} = '' unless defined $x->{rel};
907             }
908             else {
909             # now just construct the thing
910             $x = $fout{$s}{$o} = $rout{$o}{$s} = {
911             href => $o, rel => $p };
912             $x->{about} = $s if $s ne '';
913             push @out, $x;
914             }
915             }
916             }
917             }
918              
919             #warn Data::Dumper::Dumper(\%fwd);
920              
921             # XXX LOLWUT this shit reuses @_ from *this* function
922             return sort { _sort_links($a, $b) } @out;
923             }
924              
925             sub _sort_meta {
926             # first test property
927             my @p = map { defined $_->{property} ? $_->{property} : '' } @_;
928             my $t1 = $p[0] cmp $p[1];
929             return $t1 if $t1;
930              
931             # next test language
932             my @l = map { defined $_->{'xml:lang'} ? $_->{'xml:lang'} : '' } @_;
933             my $t2 = $l[0] cmp $l[1];
934             return $t2 if $t2;
935              
936             # next test datatype
937             my @d = map { defined $_->{'datatype'} ? $_->{'datatype'} : '' } @_;
938             my $t3 = $d[0] cmp $d[1];
939             return $t3 if $t3;
940              
941             # finally test content
942             my @c = map { defined $_->{'content'} ? $_->{'content'} : '' } @_;
943             # TODO numeric comparison for appropriate datatypes
944             return $d[0] cmp $d[1];
945             }
946              
947             sub _handle_metas {
948             my $metas = shift || [];
949             return @$metas if ref $metas eq 'ARRAY';
950             Carp::croak('meta must be ARRAY or HASH ref') unless ref $metas eq 'HASH';
951              
952             my %m = %$metas;
953             my %c;
954             while (my ($k, $v) = each %m) {
955             my $rv = ref $v;
956              
957             # normalize the input into something we can use
958             my @v;
959             if ($rv eq 'HASH') {
960             # keys are lang/datatype
961             for my $dt (keys %$v) {
962             my $y = $v->{$dt};
963             my @z = ref $y eq 'ARRAY' ? @$y : ($y);
964             my $l = $dt if $dt =~ /^@/;
965             undef $dt if $l;
966             map { push @v, [$_, $l, $dt] } @z;
967             }
968             }
969             else {
970             @v = $rv eq 'ARRAY' ? @$v : ($v);
971             }
972              
973             # now we turn the thing inside out
974             for my $val (@v) {
975             my ($x, $l, $dt) = ref $val eq 'ARRAY' ? @$val : ($val);
976             next unless defined $x;
977              
978             # language becomes datatype if it is set
979             if (defined $l and $l ne '') {
980             $l = "\@$l" unless $l =~ /^@/;
981             $dt = $l;
982             }
983             #$dt ||= '';
984              
985             # now we create the structure
986             my $y = $c{$v} ||= {};
987             my $z = $y->{$dt || ''} ||= {};
988             $z->{$k}++;
989             }
990             }
991              
992             # now we have meta sorted by content
993             my @out;
994             for my $content (keys %c) {
995             while (my ($dt, $preds) = each %{$c{$content}}) {
996             my %meta = (content => $content,
997             property => join ' ', sort keys %$preds);
998             if ($dt =~ /^@(.+)/) {
999             $meta{'xml:lang'} = lc $1;
1000             }
1001             else {
1002             $meta{datatype} = $dt unless $dt eq '';
1003             }
1004             push @out, \%meta;
1005             }
1006             }
1007             return sort { _sort_meta($a, $b) } @out;
1008             }
1009              
1010             sub _handle_title {
1011             my $title = shift;
1012             my $tr = ref $title;
1013             # this is a title tag but let's make sure
1014             return (%$title, -name => 'title') if $tr eq 'HASH';
1015              
1016             # this is a title tag with shorthand for predicate(s)
1017             if ($tr eq 'ARRAY') {
1018             my ($t, @p) = @{$title};
1019             my ($dt, $l);
1020             ($t, $dt, $l) = @$t if ref $t eq 'ARRAY';
1021             return (-name => 'title', -content => $t,
1022             property => join(' ', sort @p), datatype => $dt, lang => $l);
1023             }
1024              
1025             # this is anything else
1026             return (-name => 'title', -content => $title);
1027             }
1028              
1029             sub _isa_really {
1030             my ($obj, $class) = @_;
1031              
1032             defined $obj and ref $obj
1033             and Scalar::Util::blessed($obj) and $obj->isa($class);
1034             }
1035              
1036             sub _strip_ns {
1037             my $ns = shift;
1038             if (_isa_really($ns, 'URI::NamespaceMap')) {
1039             return { map +($_ => $ns->namespace_uri($_)->as_string),
1040             $ns->list_prefixes };
1041             }
1042             elsif (_isa_really($ns, 'RDF::Trine::NamespaceMap')) {
1043             return { map +($_, $ns->namespace_uri($_)->uri_value->uri_value),
1044             $ns->list_prefixes };
1045             }
1046             else {
1047             return $ns;
1048             }
1049             }
1050              
1051             sub _XHTML {
1052             my $self = shift;
1053             my %p = @_;
1054              
1055             # ns is empty if prefix has stuff in it
1056             my $nstemp = _strip_ns($p{ns} || {});
1057             my %ns = map +("xmlns:$_" => $nstemp->{$_}), keys %{$nstemp || {}}
1058             unless $p{prefix};
1059              
1060             # deal with fancy metadata
1061             my @link = _handle_links($p{link}, $p{uri});
1062             my @meta = _handle_metas($p{meta});
1063             my @head = @{$p{head} || []};
1064              
1065             # deal with title
1066             my %title = _handle_title($p{title});
1067             # deal with base
1068             my $base = { -name => 'base', href => $p{uri} } if defined $p{uri};
1069              
1070             # deal with body
1071             my %body = (-name => 'body', %{$p{attr} || {}});
1072             $body{-content} = $p{content} if defined $p{content};
1073              
1074             my @spec = (
1075             { -doctype => 'html' },
1076             { -name => 'html', xmlns => XHTMLNS, %ns,
1077             -content => [
1078             { -name => 'head',
1079             -content => [\%title, $base, @link, @meta, @head] }, \%body ] }
1080             );
1081              
1082             # prefix is empty if it is defined but false, otherwise overrides ns
1083             my $pfxtemp = _strip_ns($p{prefix}) if $p{prefix};
1084             $spec[1]{prefix} = $pfxtemp ? $pfxtemp : defined $pfxtemp ? {} : $nstemp;
1085              
1086             # add a default vocab too
1087             $spec[1]{vocab} = $p{vocab} if $p{vocab};
1088              
1089             # add transform if present
1090             unshift @spec, { -pi => 'xml-stylesheet', type => 'text/xsl',
1091             href => $p{transform} } if $p{transform};
1092              
1093             my $doc = $p{doc} || $self->_DOC;
1094             my $body = $self->_XML(
1095             doc => $doc,
1096             spec => \@spec,
1097             args => $p{args} || [],
1098             );
1099              
1100             return wantarray ? ($body, $doc) : $body;
1101             }
1102              
1103             =head1 AUTHOR
1104              
1105             Dorian Taylor, C<< >>
1106              
1107             =head1 BUGS
1108              
1109             Please report any bugs or feature requests to C
1110             rt.cpan.org>, or through the web interface at
1111             L. I
1112             will be notified, and then you'll automatically be notified of
1113             progress on your bug as I make changes.
1114              
1115             =head1 SUPPORT
1116              
1117             You can find documentation for this module with the perldoc command.
1118              
1119             perldoc Role::Markup::XML
1120              
1121             You can also look for information at:
1122              
1123             =over 4
1124              
1125             =item * RT: CPAN's request tracker (report bugs here)
1126              
1127             L
1128              
1129             =item * AnnoCPAN: Annotated CPAN documentation
1130              
1131             L
1132              
1133             =item * CPAN Ratings
1134              
1135             L
1136              
1137             =item * Search CPAN
1138              
1139             L
1140              
1141             =back
1142              
1143             =head1 SEE ALSO
1144              
1145             =over 4
1146              
1147             =item
1148              
1149             L
1150              
1151             =item
1152              
1153             L
1154              
1155             =item
1156              
1157             L
1158              
1159             =back
1160              
1161             =head1 LICENSE AND COPYRIGHT
1162              
1163             Copyright 2016 Dorian Taylor.
1164              
1165             Licensed under the Apache License, Version 2.0 (the "License"); you
1166             may not use this file except in compliance with the License. You may
1167             obtain a copy of the License at
1168             L.
1169              
1170             Unless required by applicable law or agreed to in writing, software
1171             distributed under the License is distributed on an "AS IS" BASIS,
1172             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
1173             implied. See the License for the specific language governing
1174             permissions and limitations under the License.
1175              
1176             =cut
1177              
1178             1; # End of Role::Markup::XML