File Coverage

blib/lib/Text/FrontMatter/YAML.pm
Criterion Covered Total %
statement 94 98 95.9
branch 27 40 67.5
condition 7 10 70.0
subroutine 19 19 100.0
pod 6 6 100.0
total 153 173 88.4


line stmt bran cond sub pod time code
1             package Text::FrontMatter::YAML;
2              
3 15     15   1031015 use warnings;
  15         157  
  15         531  
4 15     15   82 use strict;
  15         27  
  15         313  
5              
6 15     15   215 use 5.10.1;
  15         51  
7              
8 15     15   9847 use Data::Dumper;
  15         101724  
  15         1120  
9 15     15   122 use Carp;
  15         30  
  15         963  
10 15     15   8903 use Encode;
  15         145078  
  15         1297  
11 15     15   9091 use YAML::Tiny qw/Load/;
  15         84460  
  15         5129  
12              
13             =head1 NAME
14              
15             Text::FrontMatter::YAML - read the "YAML front matter" format
16              
17             =cut
18             our $VERSION = '0.10';
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 20     20 1 10042 my $class = shift;
134 20         48 my $self = {};
135 20         55 bless $self => $class;
136              
137 20         86 my %args = @_;
138              
139             # disallow passing incompatible arguments
140 20 50 100     225 unless (
      50        
141             (exists $args{'document_string'})
142             xor
143             (exists $args{'frontmatter_hashref'} || exists $args{'data_text'})
144             ) {
145 0         0 croak "you must pass either 'document_string', "
146             . "or 'frontmatter_hashref' and/or 'data_text'";
147             }
148              
149             # initialize from whatever we've got
150 20 100 66     94 if (exists $args{'document_string'}) {
    50          
151 13         85 $self->_init_from_string($args{'document_string'});
152             }
153             elsif (exists $args{'frontmatter_hashref'} or exists $args{'data_text'}) {
154 7         41 $self->_init_from_sections($args{'frontmatter_hashref'}, $args{'data_text'});
155             }
156             else {
157 0         0 die "internal error: didn't get any valid init arg"
158             }
159              
160 20         96 return $self;
161             }
162              
163              
164              
165             sub _init_from_sections {
166 7     7   14 my $self = shift;
167 7         15 my $hashref = shift;
168 7         16 my $data = shift;
169              
170 7         31 $self->{'yaml'} = YAML::Tiny::Dump($hashref);
171 7         1321 $self->{'data'} = $data;
172              
173 7 100       25 if (defined $hashref) {
174             # YAML::Tiny prefixes the '---' so we don't need to
175 5         20 $self->{'document'} = $self->{'yaml'};
176 5 100       22 $self->{'document'} .= "---\n" if defined($data);
177             }
178              
179 7 100       25 if (defined $data) {
180 5         18 $self->{'document'} .= $data;
181             }
182             }
183              
184              
185             sub _init_from_fh {
186 13     13   29 my $self = shift;
187 13         24 my $fh = shift;
188 13 50       43 croak "internal error: _init_from_fh() didn't get a filehandle" unless $fh;
189              
190 13         64 my $yaml_marker_re = qr/^---\s*$/;
191              
192 15     15   169 no warnings 'uninitialized';
  15         36  
  15         9468  
193 13         179 LINE: while (my $line = <$fh>) {
194 33 100       205 if ($. == 1) {
195             # first line: determine if we've got YAML or not
196 10 100       107 if ($line =~ $yaml_marker_re) {
197             # found opening marker, read YAML front matter
198 9         57 $self->{'yaml'} = '';
199 9         47 next LINE;
200             }
201             else {
202             # the whole thing's data, slurp it and go
203 1         4 local $/;
204 1         8 $self->{'data'} = $line . <$fh>;
205 1         7 last LINE;
206             }
207             }
208             else {
209             # subsequent lines
210 23 100       143 if ($line =~ $yaml_marker_re) {
211             # found closing marker, so slurp the rest of the data
212 8         30 local $/;
213 8         53 $self->{'data'} = '' . <$fh>; # '' so we always define data here
214 8         42 last LINE;
215             }
216 15         62 $self->{'yaml'} .= $line;
217             }
218             }
219             }
220              
221              
222             sub _init_from_string {
223 13     13   31 my $self = shift;
224 13         26 my $string = shift;
225              
226             # store whole document for later retrieval
227 13         66 $self->{'document'} = $string;
228              
229             # re-encode string as utf8 bytes so that open() can re-decode it to
230             # make the filehandle from it (see "Strings with code points over
231             # 0xFF may not be mapped into in-memory file handles" in perldiag).
232 13         72 my $byte_string = encode("UTF-8", $string, Encode::FB_CROAK);
233 13 50   10   2466 open my $fh, '<:encoding(UTF-8)', \$byte_string
  10     10   69  
  10         20  
  10         89  
  10         8248  
  10         24  
  10         42  
234             or die "internal error: cannot open filehandle on string, $!";
235              
236 13         9524 $self->_init_from_fh($fh);
237              
238 13         103 close $fh;
239             }
240              
241              
242             =head2 frontmatter_hashref
243              
244             frontmatter_hashref() loads the YAML in the front matter using L
245             and returns a reference to the resulting hash.
246              
247             If there is no front matter block, it returns undef.
248              
249             =cut
250              
251             sub frontmatter_hashref {
252 3     3 1 4273 my $self = shift;
253 3 50       21 croak("you can't call frontmatter_hashref as a setter") if @_;
254              
255 3 50       13 if (! defined($self->{'yaml'})) {
256 0         0 return;
257             }
258              
259 3 50       11 if (! $self->{'yaml_hashref'}) {
260 3         16 my $href = Load($self->{'yaml'});
261 3         1340 $self->{'yaml_hashref'} = $href;
262             }
263              
264 3         10 return $self->{'yaml_hashref'};
265             }
266              
267             =head2 frontmatter_text
268              
269             frontmatter_text() returns the text found the front matter block,
270             if any. The trailing triple-dash line (C<--->), if any, is removed.
271              
272             If there is no front matter block, it returns undef.
273              
274             =cut
275              
276             sub frontmatter_text {
277 10     10 1 1190 my $self = shift;
278 10 50       44 croak("you can't call frontmatter_text as a setter") if @_;
279              
280 10         35 return $self->{'yaml'};
281             }
282              
283              
284             =head2 data_fh
285              
286             data_fh() returns a filehandle whose contents are the data section of the
287             file. The filehandle will be ready for reading from the beginning. A new
288             filehandle will be returned each time data_fh() is called.
289              
290             If there is no data section, it returns undef.
291              
292             =cut
293              
294             sub data_fh {
295 4     4 1 1550 my $self = shift;
296 4 50       10 croak("you can't call data_fh as a setter") if @_;
297              
298 4 50       9 if (! defined($self->{'data'})) {
299 0         0 return;
300             }
301              
302 4         6 my $data = $self->{'data'};
303 4 50       34 open my $fh, '<', \$data
304             or die "internal error: cannot open filehandle on string, $!";
305              
306 4         9 return $fh;
307             }
308              
309              
310             =head2 data_text
311              
312             data_text() returns a string contaning the data section of the file.
313              
314             If there is no data section, it returns undef.
315              
316             =cut
317              
318             sub data_text {
319 12     12 1 7812 my $self = shift;
320 12 50       51 croak("you can't call data_text as a setter") if @_;
321              
322 12         39 return $self->{'data'};
323             }
324              
325             =head2 document_string
326              
327             document_string() returns the complete, joined front matter and data
328             sections, suitable for writing to a file.
329              
330             =cut
331              
332             sub document_string {
333 9     9 1 3299 my $self = shift;
334 9 50       34 croak("you can't call document_string as a setter") if @_;
335              
336 9         24 return $self->{'document'};
337             }
338              
339             =head1 DIAGNOSTICS
340              
341             =over 4
342              
343             =item cannot pass 'document_string' with either 'frontmatter_hashref' or 'data_text'
344              
345             When calling new(), you can't both pass in a complete document string I
346             the individual hashref and data sections.
347              
348             =item you can't call as a setter
349              
350             Once you create the object, you can't change it.
351              
352             =item internal error: ...
353              
354             Something went wrong that wasn't supposed to, and points to a bug. Please
355             report it to me at C. Thanks!
356              
357             =back
358              
359              
360             =head1 BUGS & CAVEATS
361              
362             =over 4
363              
364             =item *
365              
366             If you create an object from a string with C, and then
367             pull the string back out with document_string(), don't rely on hash keys
368             in the YAML to be ordered the same way.
369              
370             =item *
371              
372             Errors in the YAML will only be detected upon calling frontmatter_hashref(),
373             because that's the only time that L is called to parse the YAML.
374              
375             =back
376              
377             Please report bugs to me at C or
378             use the web interface at:
379              
380             L.
381              
382             I will be notified, and then you'll automatically be notified of progress
383             on your bug as I make changes.
384              
385              
386             =head1 SEE ALSO
387              
388             Jekyll - L
389              
390             L
391              
392             L
393              
394             =head1 AUTHOR
395              
396             Aaron Hall, C
397              
398             =head1 LICENSE AND COPYRIGHT
399              
400             Copyright 2013-2021 Aaron Hall.
401              
402             This library is free software; you can redistribute it and/or modify
403             it under the same terms as Perl 5.10.1.
404              
405             See L for more information.
406              
407             =cut
408              
409             1; # End of Text::FrontMatter::YAML