File Coverage

blib/lib/Pandoc/Elements.pm
Criterion Covered Total %
statement 418 549 76.1
branch 179 394 45.4
condition 56 79 70.8
subroutine 83 94 88.3
pod 6 19 31.5
total 742 1135 65.3


line stmt bran cond sub pod time code
1             package Pandoc::Elements;
2 31     31   1684091 use strict;
  31         271  
  31         881  
3             ## no critic (ProhibitNoStrict, ProhibitSubroutinePrototypes)
4 31     31   153 use warnings;
  31         67  
  31         733  
5 31     31   722 use 5.010001;
  31         103  
6              
7             our $VERSION = '0.38';
8              
9 31     31   167 use Carp;
  31         78  
  31         1934  
10 31     31   15837 use JSON qw(decode_json);
  31         308833  
  31         163  
11 31     31   4819 use Scalar::Util qw(blessed reftype);
  31         65  
  31         2067  
12 31     31   10150 use Pandoc::Walker qw(walk);
  31         76  
  31         1616  
13 31     31   12538 use Pandoc::Version;
  31         58296  
  31         18430  
14              
15              
16             our $PANDOC_VERSION; # a string like '1.16'
17             $PANDOC_VERSION ||= eval { Pandoc::Version->new($ENV{PANDOC_VERSION}) };
18              
19             # internal variables
20              
21             my $PANDOC_API_MIN = Pandoc::Version->new('1.12.3'); # since pandoc 1.12.1
22             my $PANDOC_BIN_MIN = Pandoc::Version->new('1.12.1');
23              
24             # release version => minimal required api version
25             my @REQUIRED_API = map { Pandoc::Version->new($_) }
26             '1.19' => '1.17', # pandoc 1.19 has api 1.17.0.4, compatible with api 1.17
27             '1.18' => '1.17', # pandoc 1.18 has api 1.17.0.4, compatible with api 1.17
28             '1.16' => '1.16', # pandoc 1.16 has api 1.16
29             '1.17' => '1.16', # pandoc 1.17 has api 1.16
30             ;
31              
32             sub _as_pandoc_version {
33 32 100 66 32   228 (blessed $_[0] and $_[0]->isa('Pandoc::Version'))
34             ? $_[0] : Pandoc::Version->new($_[0])
35             }
36              
37             sub pandoc_version {
38 54 100   54 1 1250 if (@_) {
    100          
39 29         51 my $doc = shift;
40 29 100       73 if (@_) {
41 9   100     70 $doc->api_version(
42             _minimum_pandoc_api_for(@_)
43             // croak "pandoc version not supported"
44             );
45             }
46 27         69 _minimum_pandoc_version_for_api($doc->api_version);
47             } elsif (defined $PANDOC_VERSION) {
48 23         54 _as_pandoc_version($PANDOC_VERSION)
49             } else {
50 2         41 $REQUIRED_API[0]
51             }
52             }
53              
54             sub _minimum_pandoc_version_for_api {
55 27     27   44 my $api = shift;
56              
57 27         38 my $version;
58              
59 27         73 foreach (grep { $_ % 2} 0 .. @REQUIRED_API) { # 1,3,...
  243         354  
60 108 100       4248 if ($api->match($REQUIRED_API[$_]) ) {
61 48 100 100     4781 if (!$version or $version > $REQUIRED_API[$_-1]) {
62 38         767 $version = $REQUIRED_API[$_-1]
63             }
64             }
65             }
66              
67 27 100 66     2116 if (!$version and $api >= $PANDOC_API_MIN) {
68 3         124 $PANDOC_BIN_MIN;
69             } else {
70 24         389 return $version;
71             }
72             }
73              
74             sub _minimum_pandoc_api_for {
75 9     9   18 my $version = _as_pandoc_version(shift);
76 9 50       388 return if @$version <= 1; # require major.minor
77              
78 9         24 foreach (grep { $_ % 2} 0 .. @REQUIRED_API) { # 1,3,...
  81         112  
79 28 100       1892 if ($version->match($REQUIRED_API[$_-1]) ) {
80 5         480 return $REQUIRED_API[$_];
81             }
82             }
83              
84             # required version is newer than any known version
85             # return the latest known api version and hope it has not changed
86 4 100       371 if ($version > $REQUIRED_API[0]) {
87 1         36 return $REQUIRED_API[1];
88             }
89              
90 3 100       112 return $version >= $PANDOC_BIN_MIN ? $PANDOC_API_MIN : undef;
91             }
92              
93             our %ELEMENTS = (
94              
95             # BLOCK ELEMENTS
96             Plain => [ Block => 'content' ],
97             Para => [ Block => 'content' ],
98             CodeBlock => [ Block => qw(attr content) ],
99             RawBlock => [ Block => qw(format content) ],
100             BlockQuote => [ Block => 'content' ],
101             OrderedList => [ Block => qw(attr content/items) ],
102             BulletList => [ Block => 'content/items' ],
103             DefinitionList => [ Block => 'content/items:[DefinitionPair]' ],
104             Header => [ Block => qw(level attr content) ],
105             HorizontalRule => ['Block'],
106             Table => [ Block => qw(caption alignment widths headers rows) ],
107             Div => [ Block => qw(attr content) ],
108             Null => ['Block'],
109             LineBlock => [ Block => qw(content) ],
110              
111             # INLINE ELEMENTS
112             Str => [ Inline => 'content' ],
113             Emph => [ Inline => 'content' ],
114             Strong => [ Inline => 'content' ],
115             Strikeout => [ Inline => 'content' ],
116             Superscript => [ Inline => 'content' ],
117             Subscript => [ Inline => 'content' ],
118             SmallCaps => [ Inline => 'content' ],
119             Quoted => [ Inline => qw(type content) ],
120             Cite => [ Inline => qw(citations:[Citation] content) ],
121             Code => [ Inline => qw(attr content) ],
122             Space => ['Inline'],
123             SoftBreak => ['Inline'],
124             LineBreak => ['Inline'],
125             Math => [ Inline => qw(type content) ],
126             RawInline => [ Inline => qw(format content) ],
127             Link => [ Inline => qw(attr content target) ],
128             Image => [ Inline => qw(attr content target) ],
129             Note => [ Inline => 'content' ],
130             Span => [ Inline => qw(attr content) ],
131              
132             # METADATA ELEMENTS
133             MetaBool => [ Meta => 'content' ],
134             MetaString => [ Meta => 'content' ],
135             MetaMap => [ Meta => 'content' ],
136             MetaInlines => [ Meta => 'content' ],
137             MetaList => [ Meta => 'content' ],
138             MetaBlocks => [ Meta => 'content' ],
139              
140             # TYPE KEYWORDS
141             map { $_ => ['Keyword'] }
142             qw(DefaultDelim Period OneParen TwoParens SingleQuote DoubleQuote
143             DisplayMath InlineMath AuthorInText SuppressAuthor NormalCitation
144             AlignLeft AlignRight AlignCenter AlignDefault DefaultStyle Example
145             Decimal LowerRoman UpperRoman LowerAlpha UpperAlpha)
146             );
147              
148 31     31   273 use parent 'Exporter';
  31         73  
  31         169  
149             our @EXPORT = (
150             keys %ELEMENTS,
151             qw(Document attributes metadata citation pandoc_version pandoc_json pandoc_query)
152             );
153             our @EXPORT_OK = ( @EXPORT, 'element' );
154              
155             # create constructor functions
156             foreach my $name ( keys %ELEMENTS ) {
157 31     31   3365 no strict 'refs'; ## no critic
  31         60  
  31         12344  
158              
159             my ( $parent, @accessors ) = @{ $ELEMENTS{$name} };
160             my $numargs = scalar @accessors;
161             my @parents = map { "Pandoc::Document::$_" } ($parent);
162             $parent = join ' ', map { "Pandoc::Document::$_" } $parent,
163             map { 'AttributesRole' } grep { $_ eq 'attr' } @accessors;
164              
165             ## no critic (ProhibitStringyEval)
166             eval "package Pandoc::Document::$name; our \@ISA = qw($parent);";
167              
168             *{ __PACKAGE__ . "::$name" } = Scalar::Util::set_prototype(
169             sub {
170 600 100   600   20168 croak "$name expects $numargs arguments, but given " . scalar @_
171             if @_ != $numargs;
172 599 100       2559 my $self = bless {
173             t => $name,
174             c => ( @_ == 1 ? $_[0] : [@_] )
175             }, "Pandoc::Document::$name";
176 599         2090 $self->set_content(@_);
177 599         2462 $self;
178             },
179             '$' x $numargs
180             );
181              
182             for ( my $i = 0 ; $i < @accessors ; $i++ ) {
183             my $member = @accessors == 1 ? "\$e->{c}" : "\$e->{c}->[$i]";
184             my $code = "my \$e = shift; $member = ( 1 == \@_ ? \$_[0] : [\@_] ) if \@_; return";
185             # auto-bless on access via accessor (TODO: move to constructor?)
186             $code .= $accessors[$i] =~ s/:\[(.+)\]$//
187             ? " [ map { bless \$_, 'Pandoc::Document::$1' } \@{$member} ];"
188             : " $member;";
189             for ( split '/', $accessors[$i] ) {
190             ## no critic
191 3 0   3   9 *{"Pandoc::Document::${name}::$_"} = eval "sub { $code }";
  3 50       12  
  3 0       11  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  25 0       47  
  25 0       44  
  25 50       145  
  0 0       0  
  0 0       0  
  0 0       0  
  2 0       89  
  2 0       9  
  2 50       10  
  0 0       0  
  0 50       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 50       0  
  4 0       41  
  4 50       11  
  4 0       8  
  43 50       127  
  39 0       83  
  98 50       284  
  67 0       137  
  67 50       275  
  4 0       12  
  21 50       184  
  21 0       48  
  24 0       245  
  24 0       59  
  24 0       73  
  22 0       78  
  1 0       4  
  1 0       3  
  17 50       42  
  30 0       75  
  29 50       107  
  14 0       45  
  0 0       0  
  0 0       0  
  0 50       0  
  10 0       21  
  11 0       61  
  11 0       52  
  1 50       7  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 100       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 50       0  
  14 0       34  
  14 0       30  
  14 0       123  
  0 50       0  
  0 0       0  
  1 0       3  
  1 0       5  
  30 50       78  
  29 0       59  
  36 50       212  
  7 0       22  
  7 50       27  
  0 0       0  
  1 50       4  
  1 0       4  
  1 50       3  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  3 50       10  
  11 50       207  
  11 0       40  
  11 0       62  
  53 0       117  
  53 0       150  
  50 0       194  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  36 0       946  
  36 50       64  
  36 50       123  
  0 100       0  
  4 50       12  
  4 100       12  
  11 0       35  
  7 50       9278  
  7 0       52  
  3 50       36  
  4 0       15  
  4 0       18  
  2 0       8  
  1 0       4  
  2         9  
  2         11  
  2         15  
  1         24  
  4         153  
  9         27  
  26         67  
  22         57  
  17         78  
  70         226  
  70         137  
  70         249  
  0         0  
  0         0  
  0         0  
  11         26  
  18         234  
  18         107  
  7         20  
  0         0  
  0         0  
  0         0  
  1         42  
  1         40  
  1         8  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  2         6  
  2         6  
  2         8  
  0         0  
  0         0  
  4         91  
  29         103  
  29         73  
  35         141  
  13         28  
  13         52  
  3         14  
  4         47  
  4         21  
  4         13  
  1         8  
  1         4  
  14         264  
  14         33  
  14         49  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
192             }
193             }
194             }
195              
196             sub element {
197 4     4 1 367 my $name = shift;
198 31     31   282 no strict 'refs'; #
  31         94  
  31         35124  
199 4 50       13 croak "undefined element" unless defined $name;
200 4 100       181 croak "unknown element $name" unless $ELEMENTS{$name};
201 3         12 &$name(@_);
202             }
203              
204             sub Document {
205              
206 53     53 1 25970 my $from_json;
207 53         96 my $arg = do {
208 53 100       240 if ( @_ == 1 ) {
    100          
    100          
209 29         53 $from_json = 1;
210 29   100     178 my $reftype = reftype $_[0] // '';
211 29 100       97 if ( $reftype eq 'ARRAY') {
    100          
212             # old JSON format
213             {
214             meta => $_[0]->[0]->{unMeta},
215 21         107 blocks => $_[0]->[1],
216             api_version => 1.16,
217             }
218             } elsif ( $reftype eq 'HASH' ) {
219 7         15 $_[0]
220             } else {
221 1         11 croak 'Document: expect array or hash reference'
222             }
223             } elsif ( @_ == 2 ) {
224             # \%meta, \@blocks
225 9         88 { meta => $_[0], blocks => $_[1] }
226             } elsif ( @_ % 2 ) {
227             # odd number of args
228 1         11 croak "Document: too many or ambiguous arguments";
229             } else {
230             # even number of args: api_version as named parameter
231 14         62 { meta => shift, blocks => shift, @_ }
232             }
233             };
234              
235             # prefer haskell-style key but accept perl-style and abbreviated key
236             my $api_version = $arg->{'pandoc-api-version'}
237             // $arg->{pandoc_api_version}
238 51   66     364 // $arg->{api_version};
      66        
239              
240             # We copy values here because $arg may not be a pure AST representation
241 51   50     311 my $doc = bless { blocks => ( $arg->{blocks} // [] ) }, 'Pandoc::Document';
242              
243             # unblessed metadata in internal format can only come from JSON
244 51   100     151 my $meta = $arg->{meta} // {};
245 51 100       215 if ($from_json) {
246 28 50       109 croak "Document metadata must be a hash" unless 'HASH' eq reftype $meta;
247             $doc->{meta} = bless {
248 28         189 map { $_ => _bless_pandoc_element( $meta->{$_} ) } keys %$meta
  36         82  
249             }, 'Pandoc::Document::Metadata';
250             } else {
251             # otherwise allow user-friendly upgrade via 'metadata' function
252 23         83 $doc->meta($meta)
253             }
254              
255 51 100 100     275 if (!defined $api_version and defined $arg->{pandoc_version}) {
256 9         23 $doc->pandoc_version($arg->{pandoc_version});
257             } else {
258 42   66     448 $doc->api_version($api_version // $REQUIRED_API[1]);
259             }
260              
261 48         244 walk $doc, \&_bless_pandoc_element;
262              
263 48         376 return $doc;
264              
265             }
266              
267             # internal helper method
268             sub _bless_pandoc_element {
269 586     586   890 my $e = shift;
270 586 50       1129 return $e unless ref $e;
271 586 100 66     2517 return $e if blessed $e and $e->isa('Pandoc::Document::Element');
272              
273             # TODO: run recursively via set_content (don't require 'walk')
274 281 100       506 if ( 'MetaMap' eq $e->{t} ) {
275 5         8 for my $v ( values %{ $e->{c} } ) {
  5         16  
276 15         23 _bless_pandoc_element( $v );
277             }
278             }
279              
280 281         720 bless $e, 'Pandoc::Document::' . $e->{t};
281 281 100       1150 $e->upgrade($e) if $e->can('upgrade');
282 281         652 return $e;
283             }
284              
285              
286             # specific accessors
287              
288 1     1   5 sub Pandoc::Document::DefinitionPair::term { $_[0]->[0] }
289 1     1   6 sub Pandoc::Document::DefinitionPair::definitions { $_[0]->[1] }
290              
291             # additional functions
292              
293             sub attributes($) {
294              
295 39     39 1 7256 my $e = Span(['',[],[]],[]); # to make use of AttributesRole
296 39         162 $e->keyvals(@_);
297              
298 39         2378 return $e->attr;
299             }
300              
301 2     2 1 1424 sub citation($) { Pandoc::Document::Citation->new( @_ ) }
302              
303             # XXX: must require rather than use Pandoc::Metadata
304             # or its attempt to use Pandoc::Elements will result in a broken state.
305             require Pandoc::Metadata;
306              
307             sub metadata($); ## no critic
308              
309             sub metadata($) { ## no critic
310 47     47 0 156 my $value = shift;
311 47 100       192 if ( !ref $value ) {
    100          
    100          
    100          
    100          
312 6   100     29 MetaString($value // '')
313             }
314             elsif ( JSON::is_bool($value) ) {
315 2         16 MetaBool($value)
316             }
317             elsif ( blessed($value) ) {
318 11 100 100     200 if ( $value->can('is_meta') and $value->is_meta ) {
    100 100        
    100 66        
    50          
319 8         57 $value
320             }
321             elsif ( $value->can('is_inline') and $value->is_inline ) {
322 1         12 MetaInlines([ $value ])
323             }
324             elsif ( $value->can('is_block') and $value->is_block ) {
325 1         52 MetaBlocks([ $value ])
326             } elsif ( $value->isa('Pandoc::Document::Metadata') ) {
327 0         0 MetaMap( { map { $_ => $value->{$_} } keys %$value } )
  0         0  
328             } else {
329 1         6 MetaString("$value")
330             }
331             }
332             elsif ( reftype $value eq 'ARRAY' ) {
333 2         18 MetaList( [ map { metadata $_ } @$value ] )
  2         7  
334             }
335             elsif ( reftype $value eq 'HASH' ) {
336 25         394 MetaMap( { map { $_ => metadata $value->{$_} } keys %$value } )
  10         41  
337             }
338             else {
339 1         13 MetaString("$value")
340             }
341             }
342              
343             sub pandoc_json($) {
344 22 50   22 1 10815 shift if $_[0] =~ /^Pandoc::/;
345              
346 22         56 my $ast = eval { decode_json( $_[0] ) };
  22         533  
347 22 50       144 if ($@) {
348 0         0 $@ =~ s/ at [^ ]+Elements\.pm line \d+//;
349 0         0 chomp $@;
350 0         0 croak $@;
351             }
352 22         69 return Document $ast;
353             }
354              
355             *pandoc_query = *Pandoc::Walker::query;
356              
357             # document element packages
358              
359             {
360              
361             package Pandoc::Document;
362 31     31   254 use strict;
  31         58  
  31         973  
363 31     31   176 use Carp 'croak';
  31         61  
  31         1652  
364 31     31   238 use Scalar::Util qw(blessed reftype);
  31         79  
  31         1482  
365 31     31   14477 use Pandoc;
  31         1736387  
  31         233  
366             our $VERSION = '0.04';
367             our @ISA = ('Pandoc::Document::Element');
368             sub blocks;
369 1     1   6 sub name { 'Document' }
370             sub meta {
371 159 100   159   10148 if (@_ > 1) {
372 24 50       631 croak "document metadata must be a hash"
373             unless 'HASH' eq reftype $_[1];
374 24         95 my $map = Pandoc::Elements::metadata($_[1])->content;
375 24         198 $_[0]->{meta} = bless $map, 'Pandoc::Document::Metadata';
376             }
377 159         708 $_[0]->{meta};
378             }
379             sub content {
380 13 50   13   437 $_[0]->{blocks} = $_[1] if @_ > 1;
381 13         125 $_[0]->{blocks};
382             }
383             *blocks = \&content;
384 1     1   12 sub is_document { 1 }
385             sub as_block {
386 0     0   0 bless { t => 'Div', c => [ {}, $_[0]->{blocks} ] }, 'Pandoc::Document::Div';
387             }
388             sub value {
389 108     108   37465 shift->meta->value(@_);
390             }
391             *metavalue = \&value;
392             sub string {
393 2     2   11 join '', map { $_->string } @{$_[0]->content}
  3         22  
  2         10  
394             }
395             sub api_version {
396 106     106   770 my $self = shift;
397 106 100       259 if ( @_ ) {
398 50         259 my $version = Pandoc::Version->new(shift);
399 50 100       2684 croak "api_version must be >= $PANDOC_API_MIN"
400             if $version < $PANDOC_API_MIN;
401 49 50       2268 croak "api_version must have major and minor part"
402             if @$version < 2;
403 49         150 $self->{'pandoc-api-version'} = $version;
404             }
405 105         300 return $self->{'pandoc-api-version'};
406             }
407             *pandoc_version = \&Pandoc::Elements::pandoc_version;
408             sub outline {
409 0     0   0 my ($self, $depth) = @_;
410 0         0 _sections( [@{$self->blocks}], $depth );
  0         0  
411             }
412             sub _sections {
413 0     0   0 my ($list, $depth) = @_;
414 0         0 my (@blocks, @sections);
415              
416             # everything up to the first Header
417 0         0 while (@$list) {
418 0 0       0 if ($list->[0]->name eq 'Header') {
419 0 0 0     0 last if !$depth or $depth >= $list->[0]->level;
420             }
421 0         0 push @blocks, shift @$list;
422             }
423              
424             # divide into sections
425 0         0 while (@$list) {
426 0         0 my $header = shift @$list;
427 0         0 my $level = $header->level;
428              
429 0         0 my @content;
430 0         0 while (@$list) {
431 0 0       0 if ($list->[0]->name eq 'Header') {
432 0 0       0 last if $list->[0]->level <= $level;
433             }
434 0         0 push @content, shift @$list;
435             }
436              
437 0 0 0     0 my $s = ($depth and $depth < $level)
438             ? { blocks => \@content }
439             : _sections(\@content,$depth);
440 0         0 push @sections, { header => $header, %$s };
441             }
442              
443 0         0 return { blocks => \@blocks, sections => \@sections };
444             }
445             sub to_pandoc {
446 0     0   0 my ($self, @args) = @_;
447 0 0 0     0 my $pandoc = (@args and blessed($args[0]) and $args[0]->isa('Pandoc'))
448             ? shift(@args) : pandoc;
449              
450 0         0 my $api_version = $self->api_version; # save
451 0         0 $self->pandoc_version( $pandoc->version );
452              
453 0         0 my $in = $self->to_json;
454 0         0 $self->api_version($api_version); # restore
455              
456 0         0 $pandoc->run( [ -f => 'json', @args ], { in => \$in, out => \my $out } );
457 0         0 return $out;
458             }
459             foreach my $format (qw(markdown latex html rst plain)) {
460 31     31   24254 no strict 'refs';
  31         67  
  31         3762  
461             *{ __PACKAGE__ . "::to_$format" } = sub {
462 0     0   0 shift()->to_pandoc( @_, '-t' => $format );
463             }
464             }
465             }
466              
467             {
468             package Pandoc::Document::Keyword;
469             our @ISA = ('Pandoc::Document::Element');
470             }
471              
472             {
473              
474             package Pandoc::Document::Element;
475 31     31   191 use strict;
  31         62  
  31         672  
476 31     31   141 use warnings;
  31         59  
  31         1412  
477             our $VERSION = $Pandoc::Document::VERSION;
478 31     31   186 use JSON ();
  31         54  
  31         565  
479 31     31   144 use Scalar::Util qw(reftype blessed);
  31         57  
  31         1775  
480 31     31   166 use Pandoc::Walker ();
  31         54  
  31         453  
481 31     31   12899 use Pandoc::Selector;
  31         75  
  31         1036  
482 31     31   14493 use subs qw(walk query transform); # Silence syntax warnings
  31         17737  
  31         193  
483              
484             sub to_json {
485 32     32 0 3431 JSON->new->utf8->canonical->convert_blessed->encode( $_[0] );
486             }
487              
488             sub TO_JSON {
489              
490             # Run everything thru this method so arrays/hashes are cloned
491             # and objects without TO_JSON methods are stringified.
492             # Required to ensure correct scalar types for Pandoc.
493              
494             # There is no easy way in Perl to tell if a scalar value is already a string or number,
495             # so we stringify all scalar values and numify/boolify as needed afterwards.
496              
497 581     581 0 1022 my ( $ast, $maybe_blessed ) = @_;
498 581 100 100     2094 if ( $maybe_blessed && blessed $ast ) {
    100          
    50          
499 193 50       822 return $ast if $ast->can('TO_JSON'); # JSON.pm will convert
500             # may have overloaded stringification! Should we check?
501             # require overload;
502             # return "$ast" if overload::Method($ast, q/""/) or overload::Method($ast, q/0+/);
503             # carp "Non-stringifiable object $ast";
504 0         0 return "$ast";
505             }
506             elsif ( 'ARRAY' eq reftype $ast ) {
507 169 100       419 return [ map { ref($_) ? TO_JSON( $_, 1 ) : "$_"; } @$ast ];
  287         662  
508             }
509             elsif ( 'HASH' eq reftype $ast ) {
510 219         685 my %ret = %$ast;
511 219         732 while ( my ( $k, $v ) = each %ret ) {
512 438 100       1352 $ret{$k} = ref($v) ? TO_JSON( $v, 1 ) : "$v";
513             }
514 219         1167 return \%ret;
515             }
516 0         0 else { return "$ast" }
517             }
518              
519 722     722 0 4504 sub name { $_[0]->{t} }
520             sub content {
521 0     0 0 0 my $e = shift;
522 0 0       0 $e->set_content(@_) if @_;
523             $e->{c}
524 0         0 }
525             sub set_content { # TODO: document this
526 584     584 0 876 my $e = shift;
527 584 100       1538 $e->{c} = @_ == 1 ? $_[0] : [@_]
528             }
529 1     1 0 5 sub is_document { 0 }
530 2     2 0 11 sub is_block { 0 }
531 3     3 0 19 sub is_inline { 0 }
532 3     3 0 22 sub is_meta { 0 }
533             sub as_block {
534 0     0 0 0 bless { t => 'Null', c => [] }, 'Pandoc::Document::Null';
535             }
536             *walk = *Pandoc::Walker::walk;
537             *query = *Pandoc::Walker::query;
538             *transform = *Pandoc::Walker::transform;
539              
540             sub string {
541              
542             # TODO: fix issue #4 to avoid this duplication
543 60 100   60 0 179 if ( $_[0]->name =~ /^(Str|Code|CodeBlock|Math|MetaString)$/ ) {
    100          
544 41         1333 return $_[0]->content;
545             }
546             elsif ( $_[0]->name =~ /^(LineBreak|SoftBreak|Space)$/ ) {
547 1         7 return ' ';
548             }
549             join '', @{
550 18         36 $_[0]->query(
551             {
552 31     31   682 'Str|Code|CodeBlock|Math|MetaString' => sub { $_->content },
553 14     14   38 'LineBreak|Space|SoftBreak' => sub { ' ' },
554             }
555 18         145 );
556             };
557             }
558              
559             sub match {
560 170     170 0 1724 my $self = shift;
561 170 100       674 my $selector = blessed $_[0] ? shift : Pandoc::Selector->new(shift);
562 170         415 $selector->match($self);
563             }
564             }
565              
566             {
567              
568             package Pandoc::Document::AttributesRole;
569 31     31   35039 use Hash::MultiValue;
  31         70855  
  31         1086  
570 31     31   202 use Scalar::Util qw(reftype blessed);
  31         56  
  31         1537  
571 31     31   167 use Carp qw(croak);
  31         103  
  31         24541  
572              
573             sub id {
574 107 100   107   686 $_[0]->attr->[0] = defined $_[1] ? "$_[1]" : "" if @_ > 1;
    100          
575 107         3458 $_[0]->attr->[0]
576             }
577              
578             sub classes {
579 0     0   0 my $e = shift;
580 0 0       0 croak 'Method classes() is not a setter' if @_;
581 0         0 warn "->classes is deprecated. Use [ split ' ', \$e->class ] instead\n";
582 0         0 $e->attr->[1]
583             }
584              
585             sub class {
586 24     24   68 my $e = shift;
587 24 100       100 if (@_) {
588             $e->attr->[1] = [
589 5         35 grep { $_ ne '' }
590 4         23 map { split qr/\s+/, $_ }
591 1 100 66     4 map { (ref $_ and reftype $_ eq 'ARRAY') ? @$_ : $_ }
  3         19  
592             @_
593             ];
594             }
595 24         40 join ' ', @{$e->attr->[1]}
  24         473  
596             }
597              
598             sub add_attribute {
599 41     41   158 my ($e, $key, $value) = @_;
600 41 100       132 if ($key eq 'id') {
    100          
601 14         50 $e->id($value);
602             } elsif ($key eq 'class') {
603 15   100     44 $value //= '';
604 15 100 100     98 $value = ["$value"] unless (reftype $value // '') eq 'ARRAY';
605 15         31 push @{$e->attr->[1]}, grep { $_ ne '' } map { split qr/\s+/, $_ } @$value;
  15         301  
  28         128  
  22         186  
606             } else {
607 12         22 push @{$e->attr->[2]}, [ $key, "$value" ];
  12         242  
608             }
609             }
610              
611             sub keyvals {
612 49     49   789 my $e = shift;
613 49 100       168 if (@_) {
614 44 100       110 my $attrs = @_ == 1 ? shift : Hash::MultiValue->new(@_);
615 44 100 66     295 unless (blessed $attrs and $attrs->isa('Hash::MultiValue')) {
616 41         253 $attrs = Hash::MultiValue->new(%$attrs);
617             }
618 44 100       2228 $e->attr->[1] = [] if exists $attrs->{class};
619 44         1501 $e->attr->[2] = [];
620 44     41   289 $attrs->each(sub { $e->add_attribute(@_) });
  41         912  
621             }
622 49         1798 my @h;
623 49 100       162 push @h, id => $e->id if $e->id ne '';
624 49 100       95 push @h, class => $e->class if @{$e->attr->[1]};
  49         1417  
625 49         103 Hash::MultiValue->new( @h, map { @$_ } @{$e->attr->[2]} );
  20         142  
  49         878  
626             }
627              
628             }
629              
630             {
631              
632             package Pandoc::Document::Block;
633             our $VERSION = $PANDOC::Document::VERSION;
634             our @ISA = ('Pandoc::Document::Element');
635 3     3   21 sub is_block { 1 }
636 1     1   6 sub as_block { $_[0] }
637             sub null { # TODO: document this (?)
638 1     1   4 %{$_[0]} = (t => 'Null', c => []);
  1         5  
639 1         9 bless $_[0], 'Pandoc::Document::Null';
640             }
641             }
642              
643             {
644              
645             package Pandoc::Document::Inline;
646             our $VERSION = $PANDOC::Document::VERSION;
647             our @ISA = ('Pandoc::Document::Element');
648 13     13   1257 sub is_inline { 1 }
649             sub as_block {
650 1     1   10 bless { t => 'Plain', c => [ $_[0] ] }, 'Pandoc::Document::Plain';
651             }
652             }
653              
654             {
655              
656             package Pandoc::Document::LinkageRole;
657             our $VERSION = $PANDOC::Document::VERSION;
658              
659             for my $Element (qw[ Link Image ]) {
660 31     31   262 no strict 'refs'; #no critic
  31         70  
  31         7726  
661             unshift @{"Pandoc::Document::${Element}::ISA"}, __PACKAGE__; # no critic
662             }
663              
664 15 100 100 15   55 sub url { my $e = shift; $e->{c}->[-1][0] = shift if @_; return $e->{c}->[-1][0] //= ""; }
  15         54  
  15         68  
665 6 100 100 6   10 sub title { my $e = shift; $e->{c}->[-1][1] = shift if @_; return $e->{c}->[-1][1] //= ""; }
  6         17  
  6         30  
666              
667             sub upgrade {
668             # prepend attributes to old-style ast
669 2         8 unshift @{ $_[0]->{c} }, [ "", [], [] ]
670 16 100   16   22 if 2 == @{ $_[0]->{c} };
  16         54  
671             }
672             }
673              
674             {
675             package Pandoc::Document::Citation;
676             our $VERSION = $PANDOC::Document::VERSION;
677              
678 31     31   231 use Carp qw[ carp croak ];
  31         76  
  31         11281  
679              
680             my %props = (
681             id => { key => 'citationId', default => '"missing"' },
682             prefix => { key => 'citationPrefix', default => '[]' },
683             suffix => { key => 'citationSuffix', default => '[]' },
684             num => { key => 'citationNoteNum', default => '0' },
685             hash => { key => 'citationHash', default => '1' },
686             mode => {
687             key => 'citationMode',
688             default => q{
689             bless(
690             { t => 'NormalCitation', c => [] },
691             'Pandoc::Document::NormalCitation'
692             )
693             },
694             },
695             );
696              
697             {
698             my $template = <<'END_OF_TEMPLATE';
699             #line 1 "method [[[method]]]()"
700             package Pandoc::Document::Citation;
701             sub [[[method]]] {
702             my $self = shift;
703             if ( @_ ) {
704             $self->{[[[key]]]} = [[[coerce]]] ( shift // [[[default]]] );
705             }
706             return [[[coerce]]] ( $self->{[[[key]]]} //= [[[default]]] );
707             }
708             no warnings 'once';
709             *[[[alias]]] = \&[[[method]]];
710             1;
711             END_OF_TEMPLATE
712              
713             while ( my ( $name, $prop ) = each %props ) {
714             $prop->{name} = $name;
715             $prop->{method} = "Pandoc::Document::Citation::$name";
716             $prop->{alias} = "Pandoc::Document::Citation::$prop->{key}";
717             $prop->{coerce}
718             = $prop->{default} =~ /^\d$/ ? '0 +'
719             : $prop->{default} =~ /^"/ ? '"" .'
720             : "";
721             {
722             ( my $source = $template ) =~ s/\Q[[[\E(\w+)\Q]]]\E/$prop->{$1}/g;
723             local $@;
724             ## no critic
725             eval $source || croak $@ . $source;
726             }
727             }
728             }
729              
730             my %accessors = map { ; $_->{name} => $_->{key} } values %props;
731              
732             sub new {
733 2     2   7 my ( $class, $arg ) = @_;
734 2         7 my $self = bless {}, $class;
735 2         12 while ( my ( $name, $key ) = each %accessors ) {
736             # coerce on access
737 12   66     221 $self->$name( $arg->{$key} // $arg->{$name} );
738             }
739 2         26 return $self;
740             }
741              
742 31     31   241 no warnings 'once';
  31         64  
  31         29452  
743             *TO_JSON = \&Pandoc::Document::Element::TO_JSON;
744             }
745              
746             # Special TO_JSON methods to coerce data to int/number/Boolean as appropriate
747             # and to downgrade document model depending on pandoc_version
748              
749             sub Pandoc::Document::to_json {
750 16     16   63926 my ($self) = @_;
751              
752 16   66     83 local $Pandoc::Elements::PANDOC_VERSION =
753             $Pandoc::Elements::PANDOC_VERSION // $self->pandoc_version;
754              
755 16         185 return Pandoc::Document::Element::to_json( $self->TO_JSON );
756             }
757              
758             sub Pandoc::Document::TO_JSON {
759 19     19   1274 my ( $self ) = @_;
760             return $self->api_version >= 1.17
761             ? { %$self }
762 19 100       67 : [ { unMeta => $self->{meta} }, $self->{blocks} ]
763             }
764              
765             sub Pandoc::Document::SoftBreak::TO_JSON {
766 14 100   14   37 if ( pandoc_version() < '1.16' ) {
767 4         370 return { t => 'Space', c => [] };
768             } else {
769 10         898 return { t => 'SoftBreak', c => [] };
770             }
771             }
772              
773             sub Pandoc::Document::LinkageRole::TO_JSON {
774 4     4   11 my $ast = Pandoc::Document::Element::TO_JSON( $_[0] );
775 4 100       10 if ( pandoc_version() < 1.16 ) {
776             # remove attributes
777 2         162 $ast->{c} = [ @{ $ast->{c} }[ 1, 2 ] ];
  2         7  
778             }
779 4         187 return $ast;
780             }
781              
782             sub Pandoc::Document::Header::TO_JSON {
783 9     9   42 my $ast = Pandoc::Document::Element::TO_JSON( $_[0] );
784              
785             # coerce heading level to int
786 9         28 $ast->{c}[0] = int( $ast->{c}[0] );
787 9         43 return $ast;
788             }
789              
790             sub Pandoc::Document::OrderedList::TO_JSON {
791 0     0   0 my $ast = Pandoc::Document::Element::TO_JSON( $_[0] );
792              
793             # coerce first item number to int
794 0         0 $ast->{c}[0][0] = int( $ast->{c}[0][0] );
795 0         0 return $ast;
796             }
797              
798             sub Pandoc::Document::Table::TO_JSON {
799 0     0   0 my $ast = Pandoc::Document::Element::TO_JSON( $_[0] );
800              
801             # coerce column widths to numbers (floats)
802 0         0 $_ += 0 for @{ $ast->{c}[2] }; # faster than map
  0         0  
803 0         0 return $ast;
804             }
805              
806             sub Pandoc::Document::Cite::TO_JSON {
807 0     0   0 my $ast = Pandoc::Document::Element::TO_JSON( $_[0] );
808 0         0 for my $citation ( @{ $ast->{c}[0] } ) {
  0         0  
809 0         0 for my $key (qw[ citationHash citationNoteNum ]) {
810              
811             # coerce to int
812 0         0 $citation->{$key} = int( $citation->{$key} );
813             }
814             }
815 0         0 return $ast;
816             }
817              
818             sub Pandoc::Document::LineBlock::TO_JSON {
819 2     2   7 my $ast = Pandoc::Document::Element::TO_JSON( $_[0] );
820 2         5 my $content = $ast->{c};
821              
822 2         5 for my $line ( @$content ) {
823              
824             # Convert spaces at the beginning of each line
825             # to Unicode non-breaking spaces, because pandoc does.
826 6 50 33     20 next unless @$line and $line->[0]->{t} eq 'Str';
827 6         27 $line->[0]->{c} =~ s{^(\x{20}+)}{ "\x{a0}" x length($1) }e;
  2         10  
828             }
829              
830 2 100       7 return $ast if pandoc_version() >= 1.18;
831              
832 1         203 my $c = [ map { ; @$_, LineBreak() } @{$content} ];
  3         8  
  1         4  
833 1         2 pop @$c; # remove trailing line break
834 1         7 return Para( $c )->TO_JSON;
835             }
836              
837             1;
838             __END__