File Coverage

blib/lib/Text/FrontMatter/YAML.pm
Criterion Covered Total %
statement 97 100 97.0
branch 35 42 83.3
condition 14 16 87.5
subroutine 19 19 100.0
pod 6 6 100.0
total 171 183 93.4


line stmt bran cond sub pod time code
1             package Text::FrontMatter::YAML;
2              
3 16     16   930528 use warnings;
  16         164  
  16         473  
4 16     16   75 use strict;
  16         24  
  16         289  
5              
6 16     16   178 use 5.10.1;
  16         59  
7              
8 16     16   8721 use Data::Dumper;
  16         93475  
  16         893  
9 16     16   102 use Carp;
  16         29  
  16         805  
10 16     16   8120 use Encode;
  16         130642  
  16         1064  
11 16     16   7395 use YAML::Tiny qw/Load/;
  16         77476  
  16         5385  
12              
13             =head1 NAME
14              
15             Text::FrontMatter::YAML - read the "YAML front matter" format
16              
17             =cut
18             our $VERSION = '1.00';
19              
20              
21             =head1 SYNOPSIS
22              
23             use File::Slurp;
24             use Text::FrontMatter::YAML;
25              
26             # READING
27             my $text_with_frontmatter = read_file("filename.md");
28             my $tfm = Text::FrontMatter::YAML->new(
29             document_string => $text_with_frontmatter
30             );
31              
32             my $hashref = $tfm->frontmatter_hashref;
33             my $mumble = $hashref->{'mumble'};
34             my $data = $tfm->data_text;
35              
36             # or also
37              
38             my $fh = $tfm->data_fh();
39             while (defined(my $line = <$fh>)) {
40             # do something with the file data
41             }
42              
43             # WRITING
44             my $tfm = Text::FrontMatter::YAML->new(
45             frontmatter_hashref => {
46             title => 'The first sentence of the "Gettysburg Address"',
47             author => 'Abraham Lincoln',
48             date => 18631119
49             },
50             data_text => "Four score and seven years ago...",
51             );
52              
53             write_file("gettysburg.md", $tfm->document_string);
54              
55              
56             =head1 DESCRIPTION
57              
58             Text::FrontMatter::YAML reads and writes files with so-called "YAML front
59             matter", such as are found on GitHub (and used in Jekyll, and various
60             other programs). It's a way of associating metadata with a file by marking
61             off the metadata into a YAML section at the top of the file. (See L
62             Structure of files with front matter> for more.)
63              
64             You can create an object from a string containing a full document (say,
65             the contents of a file), or from a hashref (to turn into the YAML front
66             matter) and a string (for the rest of the file data). The object can't be
67             altered once it's created.
68              
69             =head2 The Structure of files with front matter
70              
71             Files with a block at the beginning like the following are considered to
72             have "front matter":
73              
74             ---
75             author: Aaron Hall
76             email: ahall@vitahall.org
77             module: Text::FrontMatter::YAML
78             version: 0.50
79             ---
80             This is the rest of the file data, and isn't part of
81             the front matter block. This section of the file is not
82             interpreted in any way by Text::FrontMatter::YAML.
83              
84             It is not an error to open or create documents that have no front matter
85             block, nor those that have no data block.
86              
87             Triple-dashed lines (C<---\n>) mark the beginning of the two sections.
88             The first triple-dashed line marks the beginning of the front matter. The
89             second such line marks the beginning of the data section. Thus the
90             following is a valid document:
91              
92             ---
93             ---
94              
95             That defines a document with defined but empty front matter and data
96             sections. The triple-dashed lines are stripped when the front matter or
97             data are returned as text.
98              
99             If the input has front matter, a triple-dashed line must be the first line
100             of the file. If not, the file is considered to have no front matter; it's
101             all data. frontmatter_text() and frontmatter_hashref() will return
102             undef in this case.
103              
104             In input with a front matter block, the first line following the next
105             triple-dashed line begins the data section. If there I no second
106             triple-dashed line the file is considered to have no data section, and
107             data_text() and data_fh() will return undef.
108              
109             Creating an object with C and C works
110             in reverse, except that there's no way to specify an empty (as opposed
111             to non-existent) YAML front matter section.
112              
113             =head2 Encoding
114              
115             Text::FrontMatter::YAML operates on Perl character strings. Decode your
116             data from its original encoding before passing it in; re-encode it
117             after getting it back. See L.
118              
119             =head1 METHODS
120              
121             Except for new(), none of these methods take parameters.
122              
123             =head2 new
124              
125             new() creates a new Text::FrontMatter::YAML object. You can pass either
126             C with the full text of a document (see L
127             of files with front matter>) or one or both of C
128             and C.
129              
130             =cut
131              
132             sub new {
133 27     27 1 8259 my $class = shift;
134 27         55 my $self = {};
135 27         54 bless $self => $class;
136              
137 27         90 my %args = @_;
138              
139             # make sure we get something to init with
140 27 100 100     125 unless (
      100        
141             exists $args{'document_string'}
142             || exists $args{'frontmatter_hashref'}
143             || exists $args{'data_text'}
144             )
145             {
146 1         65 croak "must pass 'document_string', 'data_text', or 'frontmatter_hashref'";
147             }
148              
149             # disallow passing incompatible arguments
150 26 100 100     194 unless (
      75        
151             (exists $args{'document_string'})
152             xor
153             (exists $args{'frontmatter_hashref'} || exists $args{'data_text'})
154             )
155             {
156 2         200 croak "cannot pass 'document_string' with either "
157             . "'frontmatter_hashref' or 'data_text'";
158             }
159              
160             # initialize from whatever we've got
161 24 100 66     93 if (exists $args{'document_string'}) {
    50          
162 15         72 $self->_init_from_string($args{'document_string'});
163             }
164             elsif (exists $args{'frontmatter_hashref'} or exists $args{'data_text'}) {
165 9         45 $self->_init_from_sections($args{'frontmatter_hashref'}, $args{'data_text'});
166             }
167             else {
168 0         0 die "internal error: didn't get any valid init arg"
169             }
170              
171 24         98 return $self;
172             }
173              
174              
175              
176             sub _init_from_sections {
177 9     9   15 my $self = shift;
178 9         16 my $hashref = shift;
179 9         17 my $data = shift;
180              
181 9         80 $self->{'yaml'} = YAML::Tiny::Dump($hashref);
182 9         1344 $self->{'data'} = $data;
183              
184 9 100       32 if (defined $hashref) {
185             # YAML::Tiny prefixes the '---' so we don't need to
186 6         13 $self->{'document'} = $self->{'yaml'};
187 6 100       22 $self->{'document'} .= "---\n" if defined($data);
188             }
189              
190 9 100       24 if (defined $data) {
191 6         17 $self->{'document'} .= $data;
192             }
193             }
194              
195              
196             sub _init_from_fh {
197 15     15   28 my $self = shift;
198 15         20 my $fh = shift;
199 15 50       43 croak "internal error: _init_from_fh() didn't get a filehandle" unless $fh;
200              
201 15         57 my $yaml_marker_re = qr/^---\s*$/;
202              
203 16     16   127 no warnings 'uninitialized';
  16         30  
  16         8404  
204 15         169 LINE: while (my $line = <$fh>) {
205 41 100       213 if ($. == 1) {
206             # first line: determine if we've got YAML or not
207 12 100       105 if ($line =~ $yaml_marker_re) {
208             # found opening marker, read YAML front matter
209 11         52 $self->{'yaml'} = '';
210 11         47 next LINE;
211             }
212             else {
213             # the whole thing's data, slurp it and go
214 1         3 local $/;
215 1         8 $self->{'data'} = $line . <$fh>;
216 1         5 last LINE;
217             }
218             }
219             else {
220             # subsequent lines
221 29 100       129 if ($line =~ $yaml_marker_re) {
222             # found closing marker, so slurp the rest of the data
223 10         32 local $/;
224 10         50 $self->{'data'} = '' . <$fh>; # '' so we always define data here
225 10         40 last LINE;
226             }
227 19         65 $self->{'yaml'} .= $line;
228             }
229             }
230             }
231              
232              
233             sub _init_from_string {
234 15     15   30 my $self = shift;
235 15         24 my $string = shift;
236              
237             # store whole document for later retrieval
238 15         63 $self->{'document'} = $string;
239              
240             # re-encode string as utf8 bytes so that open() can re-decode it to
241             # make the filehandle from it (see "Strings with code points over
242             # 0xFF may not be mapped into in-memory file handles" in perldiag).
243 15         180 my $byte_string = encode("UTF-8", $string, Encode::FB_CROAK);
244 15 50   11   2481 open my $fh, '<:encoding(UTF-8)', \$byte_string
  11     11   54  
  11         20  
  11         59  
  11         6819  
  11         23  
  11         44  
245             or die "internal error: cannot open filehandle on string, $!";
246              
247 15         8543 $self->_init_from_fh($fh);
248              
249 15         96 close $fh;
250             }
251              
252              
253             =head2 frontmatter_hashref
254              
255             frontmatter_hashref() loads the YAML in the front matter using L
256             and returns a reference to the resulting hash.
257              
258             If there is no front matter block, it returns undef.
259              
260             =cut
261              
262             sub frontmatter_hashref {
263 4     4 1 1710 my $self = shift;
264 4 100       79 croak("you can't call frontmatter_hashref as a setter") if @_;
265              
266 3 50       12 if (! defined($self->{'yaml'})) {
267 0         0 return;
268             }
269              
270 3 50       10 if (! $self->{'yaml_hashref'}) {
271 3         53 my $href = Load($self->{'yaml'});
272 3         1151 $self->{'yaml_hashref'} = $href;
273             }
274              
275 3         10 return $self->{'yaml_hashref'};
276             }
277              
278             =head2 frontmatter_text
279              
280             frontmatter_text() returns the text found the front matter block,
281             if any. The trailing triple-dash line (C<--->), if any, is removed.
282              
283             If there is no front matter block, it returns undef.
284              
285             =cut
286              
287             sub frontmatter_text {
288 11     11 1 1399 my $self = shift;
289 11 100       96 croak("you can't call frontmatter_text as a setter") if @_;
290              
291 10         28 return $self->{'yaml'};
292             }
293              
294              
295             =head2 data_fh
296              
297             data_fh() returns a filehandle whose contents are the data section of the
298             file. The filehandle will be ready for reading from the beginning. A new
299             filehandle will be returned each time data_fh() is called.
300              
301             If there is no data section, it returns undef.
302              
303             =cut
304              
305             sub data_fh {
306 5     5 1 1447 my $self = shift;
307 5 100       77 croak("you can't call data_fh as a setter") if @_;
308              
309 4 50       7 if (! defined($self->{'data'})) {
310 0         0 return;
311             }
312              
313 4         7 my $data = $self->{'data'};
314 4 50       31 open my $fh, '<', \$data
315             or die "internal error: cannot open filehandle on string, $!";
316              
317 4         10 return $fh;
318             }
319              
320              
321             =head2 data_text
322              
323             data_text() returns a string contaning the data section of the file.
324              
325             If there is no data section, it returns undef.
326              
327             =cut
328              
329             sub data_text {
330 13     13 1 6649 my $self = shift;
331 13 100       107 croak("you can't call data_text as a setter") if @_;
332              
333 12         30 return $self->{'data'};
334             }
335              
336             =head2 document_string
337              
338             document_string() returns the complete, joined front matter and data
339             sections, suitable for writing to a file.
340              
341             =cut
342              
343             sub document_string {
344 10     10 1 3632 my $self = shift;
345 10 100       94 croak("you can't call document_string as a setter") if @_;
346              
347 9         24 return $self->{'document'};
348             }
349              
350             =head1 DIAGNOSTICS
351              
352             =over 4
353              
354             =item must pass 'document_string', 'data_text', or 'frontmatter_hashref'
355              
356             When calling new(), you have to pass in something to initialize the object.
357             You can't create the object and then set the contents.
358              
359             =item cannot pass 'document_string' with either 'frontmatter_hashref' or 'data_text'
360              
361             When calling new(), you can't both pass in a complete document string I
362             the individual hashref and data sections. Do one or the other.
363              
364             =item you can't call as a setter
365              
366             Once you create the object, you can't change it.
367              
368             =item internal error: ...
369              
370             Something went wrong that wasn't supposed to, and points to a bug. Please
371             report it to me at C. Thanks!
372              
373             =back
374              
375              
376             =head1 BUGS & CAVEATS
377              
378             =over 4
379              
380             =item *
381              
382             If you create an object from a string with C, and then
383             pull the string back out with document_string(), don't rely on hash keys
384             in the YAML to be ordered the same way.
385              
386             =item *
387              
388             Errors in the YAML will only be detected upon calling frontmatter_hashref(),
389             because that's the only time that L is called to parse the YAML.
390              
391             =back
392              
393             Please report bugs to me at C or
394             use the web interface at:
395              
396             L.
397              
398             I will be notified, and then you'll automatically be notified of progress
399             on your bug as I make changes.
400              
401              
402             =head1 SEE ALSO
403              
404             Jekyll - L
405              
406             L
407              
408             L
409              
410             =head1 AUTHOR
411              
412             Aaron Hall, C
413              
414             =head1 LICENSE AND COPYRIGHT
415              
416             Copyright 2013-2022 Aaron Hall.
417              
418             This library is free software; you can redistribute it and/or modify
419             it under the same terms as Perl 5.10.1.
420              
421             See L for more information.
422              
423             =cut
424              
425             1; # End of Text::FrontMatter::YAML