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   47211 use 5.010;
  1         3  
4 1     1   4 use strict;
  1         2  
  1         19  
5 1     1   4 use warnings FATAL => 'all';
  1         5  
  1         32  
6              
7 1     1   274 use Moo::Role;
  1         12671  
  1         4  
8 1     1   587 use namespace::autoclean;
  1         9692  
  1         4  
9              
10 1     1   121 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.06
58              
59             =cut
60              
61             our $VERSION = '0.06';
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 I of the spec. Optional; defaults
250             to the document.
251              
252             =item replace
253              
254             Suppose we're doing surgery to an existing XML document. Instead of
255             supplying a L, we can supply a node in said document which we
256             want to I. Note that this parameter is incompatible with
257             L, is meaningless for some node types (e.g. C<-doctype>), and
258             may fail in some contexts (e.g. when the node to be replaced is the
259             document).
260              
261             =item before, after
262              
263             Why stop at replacing nodes? Sometimes we need to snuggle a new set of
264             nodes up to one side or the other of a sibling at the same level.
265             B Will also fail if you
266             do things like try to add a second root element. Optional of course.
267             Once again, all these parameters, L, L, C
268             and C, are I.
269              
270             =item args
271              
272             An C reference of arguments to be passed into C
273             references embedded in the spec. Optional.
274              
275             =back
276              
277             =head3 Specification Format
278              
279             The building blocks of the spec are, unsurprisingly, C and
280             C references. The former correspond to elements and other
281             things, while the latter correspond to lists thereof. Literals become
282             text nodes, and blessed objects will be treated like strings, so it
283             helps if they have a string L. C references may be
284             used just about anywhere, and will be dereferenced recursively using
285             the supplied L until there is nothing left to dereference. It
286             is up to I to keep these data structures free of cycles.
287              
288             =over 4
289              
290             =item Elements
291              
292             Special keys designate the name and content of an element spec. These
293             are, unimaginitively, C<-name> and C<-content>. They work like so:
294              
295             { -name => 'body', -content => 'hurr' }
296              
297             # produces hurr
298              
299             Note that C<-content> can take any primitive: literal, C,
300             C or C reference, L object, etc.
301              
302             =item Attributes
303              
304             Any key is not C<-name> or C<-content> will be interpreted as an attribute.
305              
306             { -name => 'body', -content => 'hurr', class => 'lolwut' }
307              
308             # produces hurr
309              
310             When references are values of attributes, they are flattened into strings:
311              
312             { -name => 'body', -content => 'hurr', class => [qw(one two three)] }
313              
314             # produces hurr
315              
316             =item Namespaces
317              
318             If there is a colon in either the C<-name> key value or any of the
319             attribute keys, the processor will expect a namespace that corresponds
320             to that prefix. These are specified exactly as one would with ordinary
321             XML, with the use of an C attribute>. (Prefix-free C
322             attributes likewise work as expected.)
323              
324             { -name => 'svg',
325             xmlns => 'http://www.w3.org/2000/svg',
326             'xmlns:xlink' => 'http://www.w3.org/1999/xlink',
327             -content => [
328             { -name => 'a', 'xlink:href' => 'http://some.host/' },
329             ],
330             }
331              
332             # produces:
333             #
334             # xmlns:xlink="http://www.w3.org/1999/xlink">
335             #
336             #
337              
338             =item Other Nodes
339              
340             =over 4
341              
342             =item C<-pi>
343              
344             Processing instructions are designated by the special key C<-pi> and
345             accept arbitrary pseudo-attributes:
346              
347             { -pi => 'xml-stylesheet', type => 'text/xsl', href => '/my.xsl' }
348              
349             # produces
350              
351             =item C<-doctype>
352              
353             Document type declarations are designated by the special key
354             C<-doctype> and accept values for the keys C and C:
355              
356             { -doctype => 'html' }
357              
358             # produces
359              
360             =item C<-comment>
361              
362             Comments are designated by the special key C<-comment> and whatever is
363             in the value of that key:
364              
365             { -comment => 'hey you guyyyys' }
366              
367             # produces
368              
369             =back
370              
371             =item Callbacks
372              
373             Just about any part of a markup spec can be replaced by a C
374             reference, which can return any single value, including another
375             C reference. These are called in the context of C<$self>, i.e.,
376             as if they were a method of the object that does the role. The
377             L in the original method call form the subsequent input:
378              
379             sub callback {
380             my ($self, @args) = @_;
381              
382             my %node = (-name => 'section', id => $self->generate_id);
383              
384             # ...do things to %node, presumably involving @args...
385              
386             return \%node;
387             }
388              
389             sub make_xml {
390             my $self = shift;
391              
392             my $doc = $self->_DOC;
393             $self->_XML(
394             doc => $doc,
395             spec => { -name => 'p', -content => \&callback },
396             );
397              
398             return $doc;
399             }
400              
401             C references can appear in attribute values as well.
402              
403             =back
404              
405             =cut
406              
407             sub _flatten {
408             my ($self, $spec, $args) = @_;
409             if (my $ref = ref $spec) {
410             if ($ref eq 'ARRAY') {
411             return join ' ', grep { defined $_ }
412             map { $self->_flatten($_, $args) } @$spec;
413             }
414             elsif ($ref eq 'HASH') {
415             return join ' ',
416             map { join ': ', $_, $self->_flatten($spec->{$_}, $args) }
417             grep { defined $spec->{$_} } sort keys %$spec;
418             }
419             elsif ($ref eq 'CODE') {
420             return $self->_flatten($spec->($self, @$args), $args);
421             }
422             else {
423             return "$spec";
424             }
425             }
426             else {
427             return $spec;
428             }
429             }
430              
431             # figure out if the child should be some kind of table tag based on the parent
432             sub _table_tag {
433             my $parent = shift or return;
434              
435             my %is_tr = (thead => 1, tfoot => 1, tbody => 1);
436             my %is_th = (thead => 1, tfoot => 1);
437              
438             if ($parent->nodeType == 1) {
439             my $pln = $parent->localname;
440              
441             return 'tr' if $is_tr{$pln};
442              
443             if ($pln eq 'tr') {
444             my $gp = $parent->parentNode;
445             if ($gp and $gp->nodeType == 1) {
446             return $is_th{$gp->localname} ? 'th' : 'td';
447             }
448             }
449             }
450              
451             return;
452             }
453              
454             sub _attach {
455             my ($node, $parent) = @_;
456              
457             if ($node && $parent) {
458             if ($parent->nodeType == 9 and $node->nodeType == 1) {
459             $parent->setDocumentElement($node);
460             }
461             else {
462             $parent->appendChild($node);
463             }
464             }
465             $node;
466             }
467              
468             sub _replace {
469             my ($node, $target) = @_;
470              
471             if ($node && $target) {
472             $target->replaceNode($node);
473             }
474              
475             $node;
476             }
477              
478             my %ADJ = (
479             parent => sub {
480             my ($node, $parent) = @_;
481             if ($parent->nodeType == 9 and $node->nodeType == 1) {
482             $parent->setDocumentElement($node);
483             }
484             else {
485             $parent->appendChild($node);
486             }
487              
488             $node;
489             },
490             before => sub {
491             my ($node, $next) = @_;
492             my $parent = $next->parentNode;
493             $parent->insertBefore($node, $next);
494              
495             $node;
496             },
497             after => sub {
498             my ($node, $prev) = @_;
499             my $parent = $prev->parentNode;
500             $parent->insertAfter($node, $prev);
501              
502             $node;
503             },
504             replace => sub {
505             my ($node, $target) = @_;
506             my $od = $target->ownerDocument;
507             if ($target->isSameNode($od->documentElement)) {
508             if ($node->nodeType == 1) {
509             $od->removeChild($target);
510             $od->setDocumentElement($node);
511             }
512             else {
513             # this may not be an element
514             $od->insertAfter($node, $target);
515             $od->removeChild($target);
516             }
517             }
518             else {
519             $target->replaceNode($node);
520             }
521              
522             $node;
523             },
524             );
525              
526             sub _ancestor_is {
527             my ($node, $local, $ns) = @_;
528             return unless $node->nodeType == 1;
529              
530             return 1 if $node->localName eq $local
531             and (!defined($ns) or $node->namespaceURI eq $ns);
532              
533             my $parent = $node->parentNode;
534             _ancestor_is($parent, $local, $ns) if $parent and $parent->nodeType == 1;
535             }
536              
537             sub _XML {
538             my $self = shift;
539             my %p;
540             if (ref $_[0]) {
541             $p{spec} = $_[0];
542             @p{qw(parent doc)} = @_[1,2];
543             if (defined $_[3] and ref $_[3] eq 'ARRAY') {
544             $p{args} = $_[3];
545             }
546             else {
547             $p{args} = [@_[3..$#_]];
548             }
549             }
550             else {
551             %p = @_;
552             }
553              
554             $p{args} ||= [];
555              
556             my $adj;
557             for my $k (keys %ADJ) {
558             if ($p{$k}) {
559             Carp::croak('Conflicting adjacent nodes ' .
560             join ', ', sort grep { $p{$_} } keys %ADJ) if $adj;
561             Carp::croak("$k must be an XML node")
562             unless _isa_really($p{$k}, 'XML::LibXML::Node');
563              
564             $adj = $k;
565             }
566             }
567              
568             if ($adj) {
569             Carp::croak('Adjacent node must be attached to a document')
570             unless $p{$adj}->ownerDocument;
571             unless ($adj eq 'parent') {
572             Carp::croak('Replace/prev/next node must have a parent node')
573             unless $p{parent} = $p{$adj}->parentNode;
574             }
575              
576             $p{doc} ||= $p{$adj}->ownerDocument;
577             }
578             else {
579             $p{$adj = 'parent'} = $p{doc} ||= $self->_DOC;
580             }
581              
582             # $p{doc} ||= $p{parent} && $p{parent}->ownerDocument
583             # ? $p{parent}->ownerDocument : $self->_DOC;
584             # $p{parent} ||= $p{doc}; # this might be problematic
585              
586             my $node;
587              
588             my $ref = ref($p{spec}) || '';
589             if ($ref eq 'ARRAY') {
590             my $par = $adj ne 'parent' ?
591             $p{doc}->createDocumentFragment : $p{parent};
592              
593             # we add a _pseudo parent because it's the only way to
594             # propagate things like the namespace
595             my @out = map {
596             $self->_XML(spec => $_, parent => $par, _pseudo => $p{parent},
597             doc => $p{doc}, args => $p{args}) } @{$p{spec}};
598             if (@out) {
599             $ADJ{$adj}->($par, $p{$adj}) unless $adj eq 'parent';
600              
601             return wantarray ? @out : $out[-1];
602             }
603             return $p{$adj};
604             }
605             elsif ($ref eq 'CODE') {
606             return $self->_XML(spec => $p{spec}->($self, @{$p{args}}),
607             $adj => $p{$adj},
608             doc => $p{doc},
609             args => $p{args});
610             }
611             elsif ($ref eq 'HASH') {
612             # copy the spec so we don't screw it up
613             my %spec = %{$p{spec}};
614              
615             if (my $c = $spec{'-comment'}) {
616             $node = $p{doc}->createComment($self->_flatten($c, @{$p{args}}));
617              
618             return $ADJ{$adj}->($node, $p{$adj});
619             }
620             if (my $target = delete $spec{'-pi'}) {
621             # take -content over content
622             my $content = defined $spec{'-content'} ?
623             delete $spec{'-content'} : delete $spec{content};
624             my $data = join ' ', map {
625             sprintf q{%s="%s"}, $_, $self->_flatten($spec{$_}, @{$p{args}})
626             } sort keys %spec;
627              
628             $data .= ' ' .
629             $self->_flatten($content, @{$p{args}}) if defined $content;
630              
631             $node = $p{doc}->createProcessingInstruction($target, $data);
632              
633             return $ADJ{$adj}->($node, $p{$adj});
634             }
635             elsif (my $dtd = $spec{'-doctype'} || $spec{'-dtd'}) {
636             # in XML::LibXML::LazyBuilder i wrote that there is some
637             # XS issue and these values have to be explicitly passed
638             # in as undef.
639             my $public = $spec{public};
640             my $system = $spec{system};
641             $node = $p{doc}->createExternalSubset
642             ($dtd, $public || undef, $system || undef);
643             _attach($node, $p{doc});
644             return $node;
645             }
646             else {
647             # check for specified tag
648             my $tag = delete $spec{'-name'};
649              
650             my ($prefix, $local);
651             if (defined $tag) {
652             ($prefix, $local) = ($tag =~ QNAME_RE);
653             Carp::croak("Cannot make use of tag $tag")
654             unless defined $local;
655             }
656             $prefix ||= '';
657              
658             # detect appropriate table tag
659             unless ($tag ||= _table_tag($p{_pseudo} || $p{parent})) {
660             my $is_head = _ancestor_is($p{_pseudo} || $p{parent}, 'head');
661             # detect tag
662             if (defined $spec{src}) {
663             $tag = $is_head ? 'script' : 'img';
664             }
665             elsif (defined $spec{href}) {
666             $tag = $is_head ? 'link' : 'a';
667             }
668             else {
669             $tag = $is_head ? 'meta' : 'span';
670             }
671             }
672              
673             # okay generate the node
674             my %ns;
675              
676             if (my $nsuri =
677             ($p{_pseudo} || $p{parent})->lookupNamespaceURI($prefix)) {
678             $ns{$prefix} = $nsuri;
679             }
680              
681             for my $k (keys %spec) {
682             next unless $k =~ /^xmlns(?::(.*))/;
683             my $prefix = $1 || '';
684              
685             my $v = delete $spec{$k};
686             $v =~ s/^\s*(.*?)\s*$/$1/; # trim
687             $ns{$prefix} = $v;
688             }
689             $node = $self->_ELEM($tag, $p{doc}, \%ns);
690             $ADJ{$adj}->($node, $p{$adj});
691              
692             # now do the attributes
693             for my $k (sort grep { $_ =~ QNAME_RE } keys %spec) {
694              
695             my $val = $self->_flatten($spec{$k}, $p{args});
696             $node->setAttribute($k => $val) if (defined $val);
697             }
698              
699             # special handler for explicit content
700             my $content = delete $spec{'-content'};
701             return $self->_XML(
702             spec => $content,
703             parent => $node,
704             doc => $p{doc},
705             args => $p{args}
706             ) if defined $content;
707             }
708             }
709             elsif (Scalar::Util::blessed($p{spec})
710             and $p{spec}->isa('XML::LibXML::Node')) {
711             $node = $p{spec}->cloneNode(1);
712             $ADJ{$adj}->($node, $p{$adj});
713             }
714             else {
715             # spec is a text node, if defined
716             if (defined $p{spec}) {
717             $node = $p{doc}->createTextNode("$p{spec}");
718             $ADJ{$adj}->($node, $p{$adj});
719             }
720             }
721              
722             $node;
723             }
724              
725             =head2 _XHTML | %PARAMS
726              
727             Generate an XHTML+RDFa stub. Return the CbodyE> and the
728             document when called in list context, otherwise return just the
729             CbodyE> in scalar context (which can be used in subsequent
730             calls to L).
731              
732             my ($body, $doc) = $self->_XHTML(%p);
733              
734             # or
735              
736             my $body = $self->_XHTML(%p);
737              
738             =head3 Parameters
739              
740             =over 4
741              
742             =item uri
743              
744             The C attribute of the CbaseE> element.
745              
746             =item ns
747              
748             A mapping of namespace prefixes to URIs, which by default will appear
749             as I XML namespaces I the C attribute.
750              
751             =item prefix
752              
753             Also a mapping of prefixes to URIs. If this is set rather than C,
754             then the XML namespaces will I be set. Conversely, if this
755             parameter is defined but false, then I the contents of C
756             will appear in the conventional C way.
757              
758             =item vocab
759              
760             This will specify a default C attribute in the
761             ChtmlE> element, like L.
762              
763             =item title
764              
765             This can either be a literal title string, or C reference, or
766             C reference assumed to encompass the whole CtitleE>
767             element, or an C reference where the first element is the title
768             and subsequent elements are predicates.
769              
770             =item link
771              
772             This can either be an C reference of ordinary markup specs, or
773             a C reference where the keys are the C attribute and the
774             values are one or more (via C ref) URIs. In the latter form the
775             following behaviour holds:
776              
777             =over 4
778              
779             =item
780              
781             Predicates are grouped by C, folded, and sorted alphabetically.
782              
783             =item
784              
785             ClinkE> elements are sorted first lexically by the sorted
786             C, then by sorted C, then by C.
787              
788             =item
789              
790             A special empty C<""> hash key can be used to pass in another similar
791             structure whose keys represent C, or reverse predicates.
792              
793             =item
794              
795             A special C<-about> key can be used to specify another C
796             reference where the keys are subjects and the values are similar
797             structures to the one described.
798              
799             =back
800              
801             {
802             # ordinary links
803             'rel:prop' => [qw(urn:x-target:1 urn:x-target:2)],
804              
805             # special case for reverse links
806             '' => { 'rev:prop' => 'urn:x-demo-subject:id' },
807              
808             # special case for alternate subject
809             -about => {
810             'urn:x-demo-subject:id' => { 'some:property' => 'urn:x-target' } },
811             }
812              
813             The C reference form is passed along as-is.
814              
815             =item meta
816              
817             Behaves similarly to the C parameter, with the following exceptions:
818              
819             =over 4
820              
821             =item
822              
823             No C<""> or C<-about> pseudo-keys, as they are meaningless for
824             literals.
825              
826             =item
827              
828             Literal values can be expressed as an C reference of the form
829             C<[$val, $lang, $type]> with either the second or third element
830             C. They may also be represented as a C reference where
831             the keys are the language (denoted by a leading C<@>) or datatype
832             (everything else), and the values are the literal values.
833              
834             =back
835              
836             {
837             'prop:id' => ['foo', [2.3, undef, 'xsd:decimal']],
838             'exotic' => { '@en' => ['yo dawg', 'derp'] }
839             }
840              
841             =item head
842              
843             This is an optional C reference of C<> elements that
844             are neither C<> nor C<> (or, if you want,
845             additional unmolested C<> and C<> elements).
846              
847             =item attr
848              
849             These attributes (including C<-content>) will be passed into the
850             C<> element.
851              
852             =item content
853              
854             This parameter enables us to isolate the C<> content without
855             additional attributes.
856              
857             Note that setting this parameter will cause the method to return the
858             innermost, last node that is specified, rather than the C<>.
859              
860             =item transform
861              
862             This is the URI of a (e.g. XSLT) transform which will be included in a
863             processing instruction if supplied.
864              
865             =item args
866              
867             Same as C in L.
868              
869             =back
870              
871             =cut
872              
873             sub _sort_links {
874             # first test is about
875             #warn Data::Dumper::Dumper(\@_);
876             my @a = map { defined $_->{about} ? $_->{about} : '' } @_;
877             my $t1 = $a[0] cmp $a[1];
878             return $t1 if $t1;
879              
880             # then rel
881             my @rl = map { defined $_->{rel} ? $_->{rel} : '' } @_;
882             my $t2 = $rl[0] cmp $rl[1];
883             return $t2 if $t2;
884              
885             # then rev
886             my @rv = map { defined $_->{rev} ? $_->{rev} : '' } @_;
887             my $t3 = $rv[0] cmp $rv[1];
888             return $t3 if $t3;
889              
890             # then finally href
891             my @h = map { defined $_->{href} ? $_->{href} : '' } @_;
892             return $h[0] cmp $h[1];
893             }
894              
895             sub _handle_links {
896             my ($links, $uri) = @_;
897             $links ||= [];
898             return @$links if ref $links eq 'ARRAY';
899             Carp::croak('links must be ARRAY or HASH ref') unless ref $links eq 'HASH';
900              
901             my %l = %$links;
902             my %r = %{delete $l{''} || {}};
903             my %s = %{delete $l{-about} || {}};
904              
905              
906             # merge subjects; blank subject is document
907             %{$s{''} ||= {}} = (%{$s{''} || {}}, %l);
908              
909             my (%types, %titles); # map URIs to types and titles
910              
911             # accumulate predicates into a hierarchical structure of S -> O -> P
912             my (%fwd, %rev);
913             for my $s (keys %s) {
914             for my $p (keys %{$s{$s}}) {
915             my @o = ref $s{$s}{$p} eq 'ARRAY' ? @{$s{$s}{$p}} : ($s{$s}{$p});
916             for my $o (@o) {
917             my ($href, $type, $title) = ref $o eq 'ARRAY' ? @$o : ($o);
918             # XXX do a better uri match
919             $href = '' if $uri and $href eq $uri;
920             # XXX this overwrites titles oh well suck one
921             $types{$href} = $type;
922             $titles{$href} = $title;
923              
924             # accumulate the predicates
925             my $x = $fwd{$s} ||= {};
926             my $y = $rev{$href} ||= {};
927             my $z = $x->{$href} ||= $y->{$s} ||= {};
928             $z->{$p}++;
929             }
930             }
931             }
932              
933             # now do reverse links
934             for my $p (keys %r) {
935             my @o = ref $r{$p} eq 'ARRAY' ? @{$r{$p}} : ($r{$p});
936              
937             for my $o (@o) {
938             # we skip type and title because the link is reversed
939             my ($s) = ref $o eq 'ARRAY' ? @$o : ($o);
940             # XXX do a better uri match
941             $s = '' if $uri and $s eq $uri;
942             my $x = $fwd{$s} ||= {};
943             my $y = $rev{''} ||= {};
944             my $z = $x->{''} ||= $y->{$s} ||= {};
945             $z->{$p}++;
946             }
947             }
948              
949             # now we have accumulated all the predicates and aimed all the
950             # triples in the forward direction. now to construct the list.
951              
952             my (%fout, %rout, @out);
953              
954             # begin by making sure typed links point forward
955             for my $o (keys %types) {
956             for my $s (keys %{$rev{$o}}) {
957             $fout{$s} ||= {};
958             $rout{$o} ||= {};
959              
960             my $x = $fout{$s}{$o};
961             unless ($x) {
962             $x = $fout{$s}{$o} = $rout{$o}{$s} = { href => $o,
963             type => $types{$o} };
964             $x->{about} = $s if $s ne '';
965             push @out, $x;
966             }
967              
968             $x->{type} = $types{$o};
969             }
970             }
971              
972             # now do the same with titles
973             for my $o (keys %titles) {
974             for my $s (keys %{$rev{$o}}) {
975             $fout{$s} ||= {};
976             $rout{$o} ||= {};
977              
978             my $x = $fout{$s}{$o};
979             unless ($x) {
980             $x = $fout{$s}{$o} = $rout{$o}{$s} = { href => $o,
981             title => $titles{$o} };
982             $x->{about} = $s if $s ne '';
983             push @out, $x;
984             }
985              
986             $x->{title} = $titles{$o};
987             }
988             }
989              
990             # now we make sure blank subjects always face forward
991             if ($fwd{''}) {
992             for my $o (sort keys %{$fwd{''}}) {
993             $fout{''} ||= {};
994             $rout{$o} ||= {};
995              
996             my $x = $fout{''}{$o};
997             unless ($x) {
998             $x = $fout{''}{$o} = $rout{$o}{''} = { href => $o };
999             push @out, $x;
1000             }
1001             }
1002             }
1003              
1004             # now do forward predicates (this mapping is symmetric)
1005             for my $s (sort keys %fwd) {
1006             for my $o (sort keys %{$fwd{$s}}) {
1007             $fout{$s} ||= {};
1008             $rout{$o} ||= {};
1009              
1010             # collate up the predicates
1011             my $p = join ' ', sort keys %{$fwd{$s}{$o}};
1012              
1013             # first try forward
1014             my $x = $fout{$s}{$o};
1015             if ($x) {
1016             # set the link direction based on derp
1017             $x->{$x->{href} eq $o ? 'rel' : 'rev'} = $p;
1018             # make sure rel exists
1019             $x->{rel} = '' unless defined $x->{rel};
1020             }
1021             else {
1022             # then try backward
1023             $x = $rout{$s}{$o};
1024             if ($x) {
1025             # do the same thing but the other way around
1026             $x->{$x->{href} eq $o ? 'rel' : 'rev' } = $p;
1027             # and make sure rel exists
1028             $x->{rel} = '' unless defined $x->{rel};
1029             }
1030             else {
1031             # now just construct the thing
1032             $x = $fout{$s}{$o} = $rout{$o}{$s} = {
1033             href => $o, rel => $p };
1034             $x->{about} = $s if $s ne '';
1035             push @out, $x;
1036             }
1037             }
1038             }
1039             }
1040              
1041             #warn Data::Dumper::Dumper(\%fwd);
1042              
1043             # XXX LOLWUT this shit reuses @_ from *this* function
1044             return sort { _sort_links($a, $b) } @out;
1045             }
1046              
1047             sub _sort_meta {
1048             # first test property
1049             my @p = map { defined $_->{property} ? $_->{property} : '' } @_;
1050             my $t1 = $p[0] cmp $p[1];
1051             return $t1 if $t1;
1052              
1053             # next test language
1054             my @l = map { defined $_->{'xml:lang'} ? $_->{'xml:lang'} : '' } @_;
1055             my $t2 = $l[0] cmp $l[1];
1056             return $t2 if $t2;
1057              
1058             # next test datatype
1059             my @d = map { defined $_->{'datatype'} ? $_->{'datatype'} : '' } @_;
1060             my $t3 = $d[0] cmp $d[1];
1061             return $t3 if $t3;
1062              
1063             # finally test content
1064             my @c = map { defined $_->{'content'} ? $_->{'content'} : '' } @_;
1065             # TODO numeric comparison for appropriate datatypes
1066             return $d[0] cmp $d[1];
1067             }
1068              
1069             sub _handle_metas {
1070             my $metas = shift || [];
1071             return @$metas if ref $metas eq 'ARRAY';
1072             Carp::croak('meta must be ARRAY or HASH ref') unless ref $metas eq 'HASH';
1073              
1074             my %m = %$metas;
1075             my %c;
1076             while (my ($k, $v) = each %m) {
1077             my $rv = ref $v;
1078              
1079             # normalize the input into something we can use
1080             my @v;
1081             if ($rv eq 'HASH') {
1082             # keys are lang/datatype
1083             for my $dt (keys %$v) {
1084             my $y = $v->{$dt};
1085             my @z = ref $y eq 'ARRAY' ? @$y : ($y);
1086             my $l = $dt if $dt =~ /^@/;
1087             undef $dt if $l;
1088             map { push @v, [$_, $l, $dt] } @z;
1089             }
1090             }
1091             else {
1092             @v = $rv eq 'ARRAY' ? @$v : ($v);
1093             }
1094              
1095             # now we turn the thing inside out
1096             for my $val (@v) {
1097             my ($x, $l, $dt) = ref $val eq 'ARRAY' ? @$val : ($val);
1098             next unless defined $x;
1099              
1100             # language becomes datatype if it is set
1101             if (defined $l and $l ne '') {
1102             $l = "\@$l" unless $l =~ /^@/;
1103             $dt = $l;
1104             }
1105             #$dt ||= '';
1106              
1107             # now we create the structure
1108             my $y = $c{$v} ||= {};
1109             my $z = $y->{$dt || ''} ||= {};
1110             $z->{$k}++;
1111             }
1112             }
1113              
1114             # now we have meta sorted by content
1115             my @out;
1116             for my $content (keys %c) {
1117             while (my ($dt, $preds) = each %{$c{$content}}) {
1118             my %meta = (content => $content,
1119             property => join ' ', sort keys %$preds);
1120             if ($dt =~ /^@(.+)/) {
1121             $meta{'xml:lang'} = lc $1;
1122             }
1123             else {
1124             $meta{datatype} = $dt unless $dt eq '';
1125             }
1126             push @out, \%meta;
1127             }
1128             }
1129             return sort { _sort_meta($a, $b) } @out;
1130             }
1131              
1132             sub _handle_title {
1133             my $title = shift;
1134             my $tr = ref $title;
1135             # this is a title tag but let's make sure
1136             return (%$title, -name => 'title') if $tr eq 'HASH';
1137              
1138             # this is a title tag with shorthand for predicate(s)
1139             if ($tr eq 'ARRAY') {
1140             my ($t, @p) = @{$title};
1141             my ($dt, $l);
1142             ($t, $dt, $l) = @$t if ref $t eq 'ARRAY';
1143             return (-name => 'title', -content => $t,
1144             property => join(' ', sort @p), datatype => $dt, lang => $l);
1145             }
1146              
1147             # this is anything else
1148             return (-name => 'title', -content => $title);
1149             }
1150              
1151             sub _isa_really {
1152             my ($obj, $class) = @_;
1153              
1154             defined $obj and ref $obj
1155             and Scalar::Util::blessed($obj) and $obj->isa($class);
1156             }
1157              
1158             sub _strip_ns {
1159             my $ns = shift;
1160             if (_isa_really($ns, 'URI::NamespaceMap')) {
1161             return { map +($_ => $ns->namespace_uri($_)->as_string),
1162             $ns->list_prefixes };
1163             }
1164             elsif (_isa_really($ns, 'RDF::Trine::NamespaceMap')) {
1165             return { map +($_, $ns->namespace_uri($_)->uri_value->uri_value),
1166             $ns->list_prefixes };
1167             }
1168             else {
1169             return $ns;
1170             }
1171             }
1172              
1173             sub _XHTML {
1174             my $self = shift;
1175             my %p = @_;
1176              
1177             # ns is empty if prefix has stuff in it
1178             my $nstemp = _strip_ns($p{ns} || {});
1179             my %ns = map +("xmlns:$_" => $nstemp->{$_}), keys %{$nstemp || {}}
1180             unless $p{prefix};
1181              
1182             # deal with fancy metadata
1183             my @link = _handle_links($p{link}, $p{uri});
1184             my @meta = _handle_metas($p{meta});
1185             my @head = @{$p{head} || []};
1186              
1187             # deal with title
1188             my %title = _handle_title($p{title});
1189             # deal with base
1190             my $base = { -name => 'base', href => $p{uri} } if defined $p{uri};
1191              
1192             # deal with body
1193             my %body = (-name => 'body', %{$p{attr} || {}});
1194             $body{-content} = $p{content} if defined $p{content};
1195              
1196             my @spec = (
1197             { -doctype => 'html' },
1198             { -name => 'html', xmlns => XHTMLNS, %ns,
1199             -content => [
1200             { -name => 'head',
1201             -content => [\%title, $base, @link, @meta, @head] }, \%body ] }
1202             );
1203              
1204             # prefix is empty if it is defined but false, otherwise overrides ns
1205             my $pfxtemp = _strip_ns($p{prefix}) if $p{prefix};
1206             $spec[1]{prefix} = $pfxtemp ? $pfxtemp : defined $pfxtemp ? {} : $nstemp;
1207              
1208             # add a default vocab too
1209             $spec[1]{vocab} = $p{vocab} if $p{vocab};
1210              
1211             # add transform if present
1212             unshift @spec, { -pi => 'xml-stylesheet', type => 'text/xsl',
1213             href => $p{transform} } if $p{transform};
1214              
1215             my $doc = $p{doc} || $self->_DOC;
1216             my $body = $self->_XML(
1217             doc => $doc,
1218             spec => \@spec,
1219             args => $p{args} || [],
1220             );
1221              
1222             return wantarray ? ($body, $doc) : $body;
1223             }
1224              
1225             =head1 AUTHOR
1226              
1227             Dorian Taylor, C<< >>
1228              
1229             =head1 BUGS
1230              
1231             Please report any bugs or feature requests to C
1232             rt.cpan.org>, or through the web interface at
1233             L. I
1234             will be notified, and then you'll automatically be notified of
1235             progress on your bug as I make changes.
1236              
1237             =head1 SUPPORT
1238              
1239             You can find documentation for this module with the perldoc command.
1240              
1241             perldoc Role::Markup::XML
1242              
1243             You can also look for information at:
1244              
1245             =over 4
1246              
1247             =item * RT: CPAN's request tracker (report bugs here)
1248              
1249             L
1250              
1251             =item * AnnoCPAN: Annotated CPAN documentation
1252              
1253             L
1254              
1255             =item * CPAN Ratings
1256              
1257             L
1258              
1259             =item * Search CPAN
1260              
1261             L
1262              
1263             =back
1264              
1265             =head1 SEE ALSO
1266              
1267             =over 4
1268              
1269             =item
1270              
1271             L
1272              
1273             =item
1274              
1275             L
1276              
1277             =item
1278              
1279             L
1280              
1281             =back
1282              
1283             =head1 LICENSE AND COPYRIGHT
1284              
1285             Copyright 2016 Dorian Taylor.
1286              
1287             Licensed under the Apache License, Version 2.0 (the "License"); you
1288             may not use this file except in compliance with the License. You may
1289             obtain a copy of the License at
1290             L.
1291              
1292             Unless required by applicable law or agreed to in writing, software
1293             distributed under the License is distributed on an "AS IS" BASIS,
1294             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
1295             implied. See the License for the specific language governing
1296             permissions and limitations under the License.
1297              
1298             =cut
1299              
1300             1; # End of Role::Markup::XML