File Coverage

blib/lib/Text/Fragment.pm
Criterion Covered Total %
statement 182 187 97.3
branch 85 98 86.7
condition 16 22 72.7
subroutine 21 21 100.0
pod 5 5 100.0
total 309 333 92.7


line stmt bran cond sub pod time code
1             package Text::Fragment;
2              
3             our $DATE = '2016-01-29'; # DATE
4             our $VERSION = '0.09'; # VERSION
5              
6 5     5   14424 use 5.010001;
  5         19  
7 5     5   25 use strict;
  5         9  
  5         114  
8 5     5   25 use warnings;
  5         11  
  5         138  
9 5     5   3751 use Log::Any::IfLOG '$log';
  5         77  
  5         32  
10              
11 5     5   1992 use Data::Clone;
  5         5523  
  5         6418  
12              
13             require Exporter;
14             our @ISA = qw(Exporter);
15             our @EXPORT_OK = qw(
16             list_fragments
17             get_fragment
18             set_fragment_attrs
19             insert_fragment
20             delete_fragment
21             );
22              
23             our $re_id = qr/\A[A-Za-z0-9_.,:-]+\z/;
24              
25             our %SPEC;
26              
27             sub _format_quoted {
28 3     3   7 my $unquoted = shift;
29 3         5 my $res = "";
30 3         5 my $i = -1;
31 3         10 while (++$i < length($unquoted)) {
32 6         12 my $c = substr($unquoted, $i, 1);
33 6 50 33     38 if ($c eq '\\' or $c eq '"') {
    50          
34 0         0 $res .= "\\$c";
35             } elsif ($c !~ /[\x20-\x7F]/) {
36             # strip non-printables
37             } else {
38 6         18 $res .= $c;
39             }
40             }
41 3         10 qq("$res");
42             }
43              
44             sub _parse_quoted {
45 1     1   4 my $quoted = shift;
46 1         5 $quoted =~ s/\A"//; $quoted =~ s/"\z//;
  1         6  
47 1         3 my $res = "";
48 1         2 my $i = -1;
49 1         6 while (++$i < length($quoted)) {
50 2         6 my $c = substr($quoted, $i, 1);
51 2 50       8 if ($c eq '\\') {
52 0         0 $res .= substr($quoted, ++$i, 1);
53             } else {
54 2         8 $res .= $c;
55             }
56             }
57 1         10 $res;
58             }
59              
60             sub _format_attr_value {
61 8     8   14 my $val = shift;
62 8 100       41 $val =~ /\s|"|[^\x20-\x7f]/ ? _format_quoted($val) : $val;
63             }
64              
65             sub _label {
66 33     33   125 my %args = @_;
67 33   100     123 my $id = $args{id} // "";
68 33         67 my $label = $args{label}; # str
69 33         68 my $comment_style = $args{comment_style};
70 33         85 my $attrs = $args{attrs};
71              
72 33         115 my $quoted_re = qr/"(?:[^\n\r"\\]|\\[^\n\r])*"/;
73              
74 33         80 my $a_re; # regex to match attributes
75             my $ai_re; # also match attributes, but attribute id must be present
76 33 100       87 if (length $id) {
77 27         1091 $ai_re = qr/(?:\w+=\S*[ \t]+)*id=(?<id>\Q$id\E)(?:[ \t]+\w+=(?:$quoted_re|\S+))*/;
78             } else {
79 6         123 $ai_re = qr/(?:\w+=\S*[ \t]+)*id=(?<id>\S*)(?:[ \t]+\w+=(?:$quoted_re|\S+))*/;
80             }
81 33         340 $a_re = qr/(?:\w+=\S*)?(?:[ \t]+\w+=(?:$quoted_re|\S+))*/;
82              
83 33         84 my ($ts, $te); # tag start and end
84 33 100       126 if ($comment_style eq 'shell') {
    100          
    100          
    100          
    50          
85 22         42 $ts = "#";
86 22         40 $te = "";
87             } elsif ($comment_style eq 'c') {
88 3         7 $ts = "/*";
89 3         7 $te = "*/";
90             } elsif ($comment_style eq 'cpp') {
91 3         10 $ts = "//";
92 3         9 $te = "";
93             } elsif ($comment_style eq 'html') {
94 2         5 $ts = "<!--";
95 2         5 $te = "-->";
96             } elsif ($comment_style eq 'ini') {
97 3         8 $ts = ";";
98 3         5 $te = "";
99             }
100             # regex to detect fragment
101 33         1528 my $ore = qr!^(?<payload>.*?)[ \t]*\Q$ts\E[ \t]*
102             \Q$label\E[ \t]+
103             (?<attrs>$ai_re)[ \t]*
104             \Q$te\E[ \t]*(?<enl>\R|\z)!mx;
105              
106 33         2320 my $mre = qr!^\Q$ts\E[ \t]*
107             BEGIN[ \t]+\Q$label\E[ \t]+
108             (?<attrs>$ai_re)[ \t]*
109             \Q$te\E[ \t]*(?<is_multi>\R)
110             (?:
111             (?<payload>.*)
112             ^\Q$ts\E[ \t]*END[ \t]+\Q$label\E[ \t]+
113             (?:\w+=\S*[ \t]+)*id=\g{id}(?:[ \t]+\w+=\S+)*
114             [ \t]*\Q$te\E |
115             (?<payload>.*?) # without any ID at the ending comment
116             ^\Q$ts\E[ \t]*END[ \t]+\Q$label\E(?:[ \t]+$a_re)?[ \t]*
117             \Q$te\E
118             )
119             [ \t]*(?<enl>\R|\z)!msx;
120              
121             my $parse_attrs = sub {
122 18   50 18   72 my $s = shift // "";
123 18         34 my %a;
124 18         272 while ($s =~ /(\w+)=(?:($quoted_re)|(\S+))(?:\s+|\z)/g) {
125 28 100       212 $a{$1} = $2 ? _parse_quoted($2) : $3;
126             }
127 18         226 \%a;
128 33         194 };
129              
130             return {
131             one_line_pattern => $ore,
132             multi_line_pattern => $mre,
133             parse_attrs => $parse_attrs,
134             format_fragment => sub {
135 12     12   39 my %f = @_;
136              
137             # formatted attrs as string
138 12         22 my $as = "";
139 12 100       33 if (ref($f{attrs})) {
140 10         19 for (sort keys %{ $f{attrs} }) {
  10         36  
141 6         20 $as .= " " . "$_="._format_attr_value($f{attrs}{$_});
142             }
143             } else {
144 2         5 my $a = $parse_attrs->($f{attrs});
145 2         8 $as = join("", map {" $_="._format_attr_value($a->{$_})}
146 2         8 grep {$_ ne 'id'}
  4         11  
147             sort keys %$a);
148             }
149              
150 12         20 my $pl = $f{payload};
151              
152             # to keep things simple here, regardless of whether the replaced
153             # pattern contains ending newline (enl), we still format with ending
154             # newline. then we'll just need to strip ending newline if it's not
155             # needed.
156              
157 12 100 66     60 if ($f{is_multi} || $pl =~ /\R/) {
158 2 100       9 $pl .= "\n" unless $pl =~ /\R\z/;
159 2 50       23 "$ts BEGIN $label id=$id$as" . ($te ? " $te":"") . "\n" .
    50          
160             $pl .
161             "$ts END $label id=$id" . ($te ? " $te":"") . "\n";
162             } else {
163 10 100       81 "$pl $ts $label id=$id$as" . ($te ? " $te":"") . "\n";
164             }
165             },
166 33         437 };
167             }
168              
169             sub _doit {
170 35     35   135 my ($which, %args) = @_;
171              
172 35 50       247 die "BUG: invalid which"
173             unless $which =~ /\A(?:list|get|insert|delete|set_attrs)\z/;
174 35         68 my ($label_str, $label_sub);
175 35 50       121 if (ref($args{label}) eq 'CODE') {
176 0         0 $label_str = "FRAGMENT";
177 0         0 $label_sub = $args{label};
178             } else {
179 35   100     180 $label_str = $args{label} || "FRAGMENT";
180 35         88 $label_sub = \&_label;
181             }
182              
183 35         73 my $text = $args{text};
184 35 50       95 defined($text) or return [400, "Please specify text"];
185 35         66 my $id = $args{id};
186 35 100       137 if ($which =~ /\A(?:get|insert|set_attrs|delete)\z/) {
187 29 50       82 defined($id) or return [400, "Please specify id"];
188             }
189 35 100       93 if (defined $id) {
190 29 100       169 $id =~ $re_id or return [400, "Invalid ID syntax '$id', please use ".
191             "letters/numbers/dots/dashes only"];
192             }
193 34   100     156 my $attrs = $args{attrs} // {};
194 34         115 for (keys %$attrs) {
195 12 100       43 /\A\w+\z/ or return [400, "Invalid attribute name '$_', please use ".
196             "letters/numbers only"];
197 11 100       28 if (!defined($attrs->{$_})) {
198 2 50       7 if ($which eq 'set_attrs') {
199 2         4 next;
200             } else {
201 0         0 return [400, "Undefined value for attribute name '$_'"];
202             }
203             }
204             }
205              
206 33         68 my $good_pattern = $args{good_pattern};
207 33         60 my $replace_pattern = $args{replace_pattern};
208 33         66 my $top_style = $args{top_style};
209 33   100     124 my $comment_style = $args{comment_style} // "shell";
210 33 50       139 $comment_style =~ /\A(cpp|c|shell|html|ini)\z/ or return [
211             400, "Invalid comment_style '$comment_style', ".
212             "please use cpp/c/shell/html/ini"];
213 33         99 my $res = $label_sub->(id=>$id, label=>$label_str,
214             comment_style=>$comment_style);
215 33         81 my $one_line_pattern = $res->{one_line_pattern};
216 33         58 my $multi_line_pattern = $res->{multi_line_pattern};
217 33         52 my $parse_attrs = $res->{parse_attrs};
218 33         60 my $format_fragment = $res->{format_fragment};
219 33         58 my $payload = $args{payload};
220 33 100       109 if ($which eq 'insert') {
221 12 50       26 defined($payload) or return [400, "Please specify payload"];
222             }
223              
224 33 100       148 if ($which eq 'list') {
    100          
    100          
    100          
225              
226 6         9 my @ff;
227 6         763 while ($text =~ /($one_line_pattern|$multi_line_pattern)/xg) {
228             push @ff, {
229             raw => $1,
230 5     5   1919 id => $+{id},
  5         1780  
  5         5966  
231             payload => $+{payload},
232 12         142 attrs => $parse_attrs->($+{attrs}),
233             };
234             }
235 6         211 return [200, "OK", \@ff];
236              
237             } elsif ($which eq 'get') {
238              
239 3 100       241 if ($text =~ /($one_line_pattern|$multi_line_pattern)/x) {
240             return [200, "OK", {
241             raw => $1,
242             id => $+{id},
243             payload => $+{payload},
244 2         23 attrs => $parse_attrs->($+{attrs}),
245             }];
246             } else {
247 1         28 return [404, "Fragment with ID '$id' not found"];
248             }
249              
250             } elsif ($which eq 'set_attrs') {
251              
252 3         4 my $orig_attrs;
253             my $sub = sub {
254 2     2   16 my %f = @_;
255 2         6 $orig_attrs = $parse_attrs->($f{attrs});
256 2         8 my %a = %$orig_attrs; delete $a{id};
  2         5  
257 2         4 for my $k (keys %$attrs) {
258 6         11 my $v = $attrs->{$k};
259 6 100       10 if (defined $v) {
260 4         7 $a{$k} = $v;
261             } else {
262 2         4 delete $a{$k};
263             }
264             }
265 2         5 $f{attrs} = \%a;
266 2         6 $format_fragment->(%f);
267 3         10 };
268 3 100       238 if ($text =~ s{$one_line_pattern | $multi_line_pattern}
269 2         25 {$sub->(%+)}egx) {
270 2         55 return [200, "OK", {text=>$text, orig_attrs=>$orig_attrs}];
271             } else {
272 1         30 return [404, "Fragment with ID '$id' not found"];
273             }
274              
275             } elsif ($which eq 'delete') {
276              
277 9         15 my %f;
278             my $sub = sub {
279 8     8   96 %f = @_;
280 8 100       81 $f{enl} ? $f{bnl} : "";
281 9         27 };
282 9 100       676 if ($text =~ s{(?<bnl>\R?)
283             (?<fragment>$one_line_pattern | $multi_line_pattern)}
284 8         132 {$sub->(%+)}egx) {
285             return [200, "OK", {text=>$text,
286             orig_fragment=>$f{fragment},
287 7         198 orig_payload=>$f{payload}}];
288             } else {
289 2         57 return [304, "Fragment with ID '$id' already does not exist"];
290             }
291              
292             } else { # insert
293              
294 12         17 my $replaced;
295             my %f;
296             my $sub = sub {
297 3     3   29 %f = @_;
298 3 100       15 return $f{fragment} if $payload eq $f{payload};
299 2         5 $replaced++;
300 2         5 $f{orig_fragment} = $f{fragment};
301 2         5 $f{orig_payload} = $f{payload};
302 2         3 $f{payload} = $payload;
303 2         7 $format_fragment->(%f);
304 12         35 };
305 12 100 66     42 if ($good_pattern && $text =~ /$good_pattern/) {
306 1         18 return [304, "Text contains good pattern"];
307             }
308              
309 11 100       1081 if ($text =~ s{(?<fragment>(?:$one_line_pattern | $multi_line_pattern))}
310 3         42 {$sub->(%+)}ex) {
311 3 100       10 if ($replaced) {
312             return [200, "Payload replaced", {
313             text=>$text, orig_fragment=>$f{orig_fragment},
314 2         48 orig_payload=>$f{orig_payload}}];
315             } else {
316 1         31 return [304, "Fragment with ID '$id' already exist with ".
317             "same content"];
318             }
319             }
320              
321 8         39 my $fragment = $format_fragment->(payload=>$payload, attrs=>$attrs);
322 8 100 66     35 if ($replace_pattern && $text =~ /($replace_pattern)/) {
323 1         4 my $orig_fragment = $1;
324 1         7 $text =~ s/$replace_pattern/$fragment/;
325 1         21 return [200, "Replace pattern replaced", {
326             text=>$text, orig_fragment=>$orig_fragment}];
327             }
328              
329 7 100       18 if ($top_style) {
    100          
330 1         3 $text = $fragment . $text;
331             } elsif (length($text)) {
332 5         18 my $enl = $text =~ /\R\z/; # text ends with newline
333 5 100       22 $fragment =~ s/\R\z// unless $enl;
334 5 100       17 $text .= ($enl ? "" : "\n") . $fragment;
335             } else {
336             # insert at bottom of empty string
337 1         3 $text = $fragment;
338             }
339 7 100       135 return [200, "Fragment inserted at the ".
340             ($top_style ? "top" : "bottom"), {text=>$text}];
341             }
342              
343             }
344              
345             $SPEC{':package'} = {
346             v => 1.1,
347             summary => 'Manipulate fragments in text',
348             description => <<'_',
349              
350             A fragment is a single line or a group of lines (called payload) with a metadata
351             encoded in the comment that is put adjacent to it (for a single line fragment)
352             or enclosing it (for a multiline fragment). Fragments are usually used in
353             configuration files or code. Here is the structure of a single-line fragment:
354              
355             <payload> # <label> <attrs>
356              
357             Here is the structure of a multi-line fragment:
358              
359             # BEGIN <label> <attrs>
360             <payload>
361             # END <label> [<attrs>]
362              
363             Label is by default `FRAGMENT` but can be other string. Attributes are a
364             sequence of `name=val` separated by whitespace, where name must be alphanums
365             only and val is zero or more non-whitespace characters. There must at least be
366             an attribute with name `id`, it is used to identify fragment and allow the
367             fragment to be easily replaced/modified/deleted from text. Attributes are
368             optional in the ending comment.
369              
370             Comment character used is by default `#` (`shell`-style comment), but other
371             comment styles are supported (see below).
372              
373             Examples of single-line fragments (the second example uses `c`-style comment and
374             the third uses `cpp`-style comment):
375              
376             RSYNC_ENABLE=1 # FRAGMENT id=enable
377             some text /* FRAGMENT id=id2 */
378             some text // FRAGMENT id=id3 foo=1 bar=2
379              
380             An example of multi-line fragment (using `html`-style comment instead of
381             `shell`):
382              
383             <!-- BEGIN FRAGMENT id=id4 -->
384             some
385             lines
386             of
387             text
388             <!-- END FRAGMENT id=id4 -->
389              
390             Another example (using `ini`-style comment):
391              
392             ; BEGIN FRAGMENT id=default-settings
393             register_globals=On
394             extension=mysql.so
395             extension=gd.so
396             memory_limit=256M
397             post_max_size=64M
398             upload_max_filesize=64M
399             browscap=/c/share/php/browscap.ini
400             allow_url_fopen=0
401             ; END FRAGMENT
402              
403             _
404             };
405              
406             my $arg_comment_style = {
407             summary => 'Comment style',
408             schema => ['str' => {
409             default => 'shell',
410             in => [qw/c cpp html shell ini/],
411             }],
412             };
413             my $arg_label = {
414             schema => [str => {default=>'FRAGMENT'}],
415             summary => 'Comment label',
416             };
417              
418             my $arg_id = {
419             summary => 'Fragment ID',
420             schema => ['str*' => { match => qr/\A[\w-]+\z/ }],
421             req => 1,
422             };
423              
424             my $arg_payload = {
425             summary => 'Fragment content',
426             schema => 'str*',
427             req => 1,
428             };
429              
430             $SPEC{list_fragments} = {
431             v => 1.1,
432             summary => 'List fragments in text',
433             args => {
434             text => {
435             summary => 'The text which contain fragments',
436             schema => 'str*',
437             req => 1,
438             pos => 0,
439             },
440             comment_style => $arg_comment_style,
441             label => $arg_label,
442             },
443             result => {
444             summary => 'List of fragments',
445             schema => 'array*',
446             description => <<'_',
447              
448             Will return status 200 if operation is successful. Result will be an array of
449             fragments, where each fragment is a hash containing these keys: `raw` (string),
450             `payload` (string), `attrs` (hash), `id` (string, can also be found in
451             attributes).
452              
453             _
454             },
455             };
456             sub list_fragments {
457 6     6 1 23364 _doit('list', @_);
458             }
459              
460             $SPEC{get_fragment} = {
461             v => 1.1,
462             summary => 'Get fragment with a certain ID in text',
463             description => <<'_',
464              
465             If there are multiple occurences of the fragment with the same ID ,
466              
467             _
468             args => {
469             text => {
470             summary => 'The text which contain fragments',
471             schema => 'str*',
472             req => 1,
473             pos => 0,
474             },
475             comment_style => $arg_comment_style,
476             label => $arg_label,
477             id => $arg_id,
478             },
479             result => {
480             summary => 'Fragment',
481             schema => 'array*',
482             description => <<'_',
483              
484             Will return status 200 if fragment is found. Result will be a hash with the
485             following keys: `raw` (string), `payload` (string), `attrs` (hash), `id`
486             (string, can also be found in attributes).
487              
488             Return 404 if fragment is not found.
489              
490             _
491             },
492             };
493             sub get_fragment {
494 3     3 1 7481 _doit('get', @_);
495             }
496              
497             $SPEC{set_fragment_attrs} = {
498             v => 1.1,
499             summary => 'Set/unset attributes of a fragment',
500             description => <<'_',
501              
502             If there are multiple occurences of the fragment with the same ID ,
503              
504             _
505             args => {
506             text => {
507             summary => 'The text which contain fragments',
508             schema => 'str*',
509             req => 1,
510             pos => 0,
511             },
512             comment_style => $arg_comment_style,
513             label => $arg_label,
514             id => $arg_id,
515             attrs => {
516             schema => 'hash*',
517             description => <<'_',
518              
519             To delete an attribute in the fragment, you can set the value to undef.
520              
521             _
522             req => 1,
523             },
524             },
525             result => {
526             summary => 'New text and other data',
527             schema => 'array*',
528             description => <<'_',
529              
530             Will return status 200 if fragment is found. Result will be a hash containing
531             these keys: `text` (string, the modified text), `orig_attrs` (hash, the old
532             attributes before being modified).
533              
534             Return 404 if fragment is not found.
535              
536             _
537             },
538             };
539             sub set_fragment_attrs {
540 4     4 1 9883 _doit('set_attrs', @_);
541             }
542              
543             $SPEC{insert_fragment} = {
544             v => 1.1,
545             summary => 'Insert or replace a fragment in text',
546             description => <<'_',
547              
548             Newline insertion behaviour: if fragment is inserted at the bottom and text does
549             not end with newline (which is considered bad style), the inserted fragment will
550             also not end with newline. Except when original text is an empty string, in
551             which case an ending newline will still be added.
552              
553             _
554             args => {
555             text => {
556             summary => 'The text to insert fragment into',
557             schema => 'str*',
558             req => 1,
559             pos => 0,
560             },
561             id => $arg_id,
562             payload => $arg_payload,
563             top_style => {
564             summary => 'Whether to append fragment at beginning of file '.
565             'instead of at the end',
566             schema => [bool => { default=>0 }],
567             description => <<'_',
568              
569             Default is false, which means to append at the end of file.
570              
571             Note that this only has effect if `replace_pattern` is not defined or replace
572             pattern is not found in file. Otherwise, fragment will be inserted to replace
573             the pattern.
574              
575             _
576             },
577             replace_pattern => {
578             summary => 'Regex pattern which if found will be used for '.
579             'placement of fragment',
580             schema => 'str',
581             description => <<'_',
582              
583             If fragment needs to be inserted into file, then if `replace_pattern` is defined
584             then it will be searched. If found, fragment will be placed to replace the
585             pattern. Otherwise, fragment will be inserted at the end (or beginning, see
586             `top_style`) of file.
587              
588             _
589             },
590             good_pattern => {
591             summary => 'Regex pattern which if found means fragment '.
592             'need not be inserted',
593             schema => 'str',
594             },
595             comment_style => $arg_comment_style,
596             label => $arg_label,
597             attrs => {
598             schema => [hash => {default=>{}}],
599             },
600             },
601             result => {
602             summary => 'A hash of result',
603             schema => 'hash*',
604             description => <<'_',
605              
606             Will return status 200 if operation is successful and text is changed. The
607             result is a hash with the following keys: `text` will contain the new text,
608             `orig_payload` will contain the original payload before being removed/replaced,
609             `orig_fragment` will contain the original fragment (or the text that matches
610             `replace_pattern`).
611              
612              
613             Will return status 304 if nothing is changed (i.e. if fragment with the
614             same payload that needs to be inserted already exists in the text).
615              
616             _
617             },
618             };
619             sub insert_fragment {
620 13     13 1 40741 _doit('insert', @_);
621             }
622              
623             $SPEC{delete_fragment} = {
624             v => 1.1,
625             summary => 'Delete fragment in text',
626             description => <<'_',
627              
628             If there are multiple occurences of fragment (which is considered an abnormal
629             condition), all occurences will be deleted.
630              
631             Newline deletion behaviour: if fragment at the bottom of text does not end with
632             newline (which is considered bad style), the text after the fragment is deleted
633             will also not end with newline.
634              
635             _
636             args => {
637             text => {
638             summary => 'The text to delete fragment from',
639             schema => 'str*',
640             req => 1,
641             pos => 0,
642             },
643             id => {
644             summary => 'Fragment ID',
645             schema => ['str*' => { match => qr/\A[\w-]+\z/ }],
646             req => 1,
647             pos => 1,
648             },
649             comment_style => $arg_comment_style,
650             label => {
651             schema => ['any' => {
652             of => ['str*', 'code*'],
653             default => 'FRAGMENT',
654             }],
655             summary => 'Comment label',
656             },
657             },
658             result => {
659             summary => 'A hash of result',
660             schema => 'hash*',
661             description => <<'_',
662              
663             Will return status 200 if operation is successful and text is deleted. The
664             result is a hash with the following keys: `text` will contain the new text,
665             `orig_payload` will contain the original fragment payload before being deleted,
666             `orig_fragment` will contain the original fragment. If there are multiple
667             occurences (which is considered an abnormal condition), only the last deleted
668             fragment will be returned in `orig_payload` and `orig_fragment`.
669              
670             Will return status 304 if nothing is changed (i.e. when the fragment that needs
671             to be deleted already does not exist in the text).
672              
673             _
674             },
675             };
676             sub delete_fragment {
677 9     9 1 40020 _doit('delete', @_);
678             }
679              
680             1;
681             # ABSTRACT: Manipulate fragments in text
682              
683             __END__
684              
685             =pod
686              
687             =encoding UTF-8
688              
689             =head1 NAME
690              
691             Text::Fragment - Manipulate fragments in text
692              
693             =head1 VERSION
694              
695             This document describes version 0.09 of Text::Fragment (from Perl distribution Text-Fragment), released on 2016-01-29.
696              
697             =head1 SYNOPSIS
698              
699             use Text::Fragment qw(list_fragments
700             get_fragment
701             set_fragment_attrs
702             insert_fragment
703             delete_fragment);
704              
705             my $text = <<_;
706             foo = "some value"
707             baz = 0
708             _
709              
710             To insert a fragment:
711              
712             my $res = insert_fragment(text=>$text, id=>'bar', payload=>'bar = 2');
713              
714             C<< $res->[2]{text} >> will now contain:
715              
716             foo = "some value"
717             baz = 0
718             bar = 2 # FRAGMENT id=bar
719              
720             To replace a fragment:
721              
722             $res = insert_fragment(text=>$res->[2], id='bar', payload=>'bar = 3');
723              
724             C<< $res->[2]{text} >> will now contain:
725              
726             foo = "some value"
727             baz = 0
728             bar = 3 # FRAGMENT id=bar
729              
730             and C<< $res->[2]{orig_payload} >> will contain the payload before being
731             replaced:
732              
733             bar = 2
734              
735             To delete a fragment:
736              
737             $res = delete_fragment(text=>$res->[2], id=>'bar');
738              
739             To list fragments:
740              
741             $res = list_fragment(text=>$text);
742              
743             To get a fragment:
744              
745             $res = get_fragment(text=>$text, id=>'bar');
746              
747             To set fragment attributes:
748              
749             $res = se_fragment_attrs(text=>$text, id=>'bar', attrs=>{name=>'val', ...});
750              
751             =head1 DESCRIPTION
752              
753              
754             A fragment is a single line or a group of lines (called payload) with a metadata
755             encoded in the comment that is put adjacent to it (for a single line fragment)
756             or enclosing it (for a multiline fragment). Fragments are usually used in
757             configuration files or code. Here is the structure of a single-line fragment:
758              
759             <payload> # <label> <attrs>
760              
761             Here is the structure of a multi-line fragment:
762              
763             # BEGIN <label> <attrs>
764             <payload>
765             # END <label> [<attrs>]
766              
767             Label is by default C<FRAGMENT> but can be other string. Attributes are a
768             sequence of C<name=val> separated by whitespace, where name must be alphanums
769             only and val is zero or more non-whitespace characters. There must at least be
770             an attribute with name C<id>, it is used to identify fragment and allow the
771             fragment to be easily replaced/modified/deleted from text. Attributes are
772             optional in the ending comment.
773              
774             Comment character used is by default C<#> (C<shell>-style comment), but other
775             comment styles are supported (see below).
776              
777             Examples of single-line fragments (the second example uses C<c>-style comment and
778             the third uses C<cpp>-style comment):
779              
780             RSYNC_ENABLE=1 # FRAGMENT id=enable
781             some text /* FRAGMENT id=id2 */
782             some text // FRAGMENT id=id3 foo=1 bar=2
783              
784             An example of multi-line fragment (using C<html>-style comment instead of
785             C<shell>):
786              
787             <!-- BEGIN FRAGMENT id=id4 -->
788             some
789             lines
790             of
791             text
792             <!-- END FRAGMENT id=id4 -->
793              
794             Another example (using C<ini>-style comment):
795              
796             ; BEGIN FRAGMENT id=default-settings
797             register_globals=On
798             extension=mysql.so
799             extension=gd.so
800             memory_limit=256M
801             post_max_size=64M
802             upload_max_filesize=64M
803             browscap=/c/share/php/browscap.ini
804             allow_url_fopen=0
805             ; END FRAGMENT
806              
807             =head1 FUNCTIONS
808              
809              
810             =head2 delete_fragment(%args) -> [status, msg, result, meta]
811              
812             Delete fragment in text.
813              
814             If there are multiple occurences of fragment (which is considered an abnormal
815             condition), all occurences will be deleted.
816              
817             Newline deletion behaviour: if fragment at the bottom of text does not end with
818             newline (which is considered bad style), the text after the fragment is deleted
819             will also not end with newline.
820              
821             This function is not exported by default, but exportable.
822              
823             Arguments ('*' denotes required arguments):
824              
825             =over 4
826              
827             =item * B<comment_style> => I<str> (default: "shell")
828              
829             Comment style.
830              
831             =item * B<id>* => I<str>
832              
833             Fragment ID.
834              
835             =item * B<label> => I<str|code> (default: "FRAGMENT")
836              
837             Comment label.
838              
839             =item * B<text>* => I<str>
840              
841             The text to delete fragment from.
842              
843             =back
844              
845             Returns an enveloped result (an array).
846              
847             First element (status) is an integer containing HTTP status code
848             (200 means OK, 4xx caller error, 5xx function error). Second element
849             (msg) is a string containing error message, or 'OK' if status is
850             200. Third element (result) is optional, the actual result. Fourth
851             element (meta) is called result metadata and is optional, a hash
852             that contains extra information.
853              
854             Return value: A hash of result (hash)
855              
856              
857             Will return status 200 if operation is successful and text is deleted. The
858             result is a hash with the following keys: C<text> will contain the new text,
859             C<orig_payload> will contain the original fragment payload before being deleted,
860             C<orig_fragment> will contain the original fragment. If there are multiple
861             occurences (which is considered an abnormal condition), only the last deleted
862             fragment will be returned in C<orig_payload> and C<orig_fragment>.
863              
864             Will return status 304 if nothing is changed (i.e. when the fragment that needs
865             to be deleted already does not exist in the text).
866              
867              
868             =head2 get_fragment(%args) -> [status, msg, result, meta]
869              
870             Get fragment with a certain ID in text.
871              
872             If there are multiple occurences of the fragment with the same ID ,
873              
874             This function is not exported by default, but exportable.
875              
876             Arguments ('*' denotes required arguments):
877              
878             =over 4
879              
880             =item * B<comment_style> => I<str> (default: "shell")
881              
882             Comment style.
883              
884             =item * B<id>* => I<str>
885              
886             Fragment ID.
887              
888             =item * B<label> => I<str> (default: "FRAGMENT")
889              
890             Comment label.
891              
892             =item * B<text>* => I<str>
893              
894             The text which contain fragments.
895              
896             =back
897              
898             Returns an enveloped result (an array).
899              
900             First element (status) is an integer containing HTTP status code
901             (200 means OK, 4xx caller error, 5xx function error). Second element
902             (msg) is a string containing error message, or 'OK' if status is
903             200. Third element (result) is optional, the actual result. Fourth
904             element (meta) is called result metadata and is optional, a hash
905             that contains extra information.
906              
907             Return value: Fragment (array)
908              
909              
910             Will return status 200 if fragment is found. Result will be a hash with the
911             following keys: C<raw> (string), C<payload> (string), C<attrs> (hash), C<id>
912             (string, can also be found in attributes).
913              
914             Return 404 if fragment is not found.
915              
916              
917             =head2 insert_fragment(%args) -> [status, msg, result, meta]
918              
919             Insert or replace a fragment in text.
920              
921             Newline insertion behaviour: if fragment is inserted at the bottom and text does
922             not end with newline (which is considered bad style), the inserted fragment will
923             also not end with newline. Except when original text is an empty string, in
924             which case an ending newline will still be added.
925              
926             This function is not exported by default, but exportable.
927              
928             Arguments ('*' denotes required arguments):
929              
930             =over 4
931              
932             =item * B<attrs> => I<hash> (default: {})
933              
934             =item * B<comment_style> => I<str> (default: "shell")
935              
936             Comment style.
937              
938             =item * B<good_pattern> => I<str>
939              
940             Regex pattern which if found means fragment need not be inserted.
941              
942             =item * B<id>* => I<str>
943              
944             Fragment ID.
945              
946             =item * B<label> => I<str> (default: "FRAGMENT")
947              
948             Comment label.
949              
950             =item * B<payload>* => I<str>
951              
952             Fragment content.
953              
954             =item * B<replace_pattern> => I<str>
955              
956             Regex pattern which if found will be used for placement of fragment.
957              
958             If fragment needs to be inserted into file, then if C<replace_pattern> is defined
959             then it will be searched. If found, fragment will be placed to replace the
960             pattern. Otherwise, fragment will be inserted at the end (or beginning, see
961             C<top_style>) of file.
962              
963             =item * B<text>* => I<str>
964              
965             The text to insert fragment into.
966              
967             =item * B<top_style> => I<bool> (default: 0)
968              
969             Whether to append fragment at beginning of file instead of at the end.
970              
971             Default is false, which means to append at the end of file.
972              
973             Note that this only has effect if C<replace_pattern> is not defined or replace
974             pattern is not found in file. Otherwise, fragment will be inserted to replace
975             the pattern.
976              
977             =back
978              
979             Returns an enveloped result (an array).
980              
981             First element (status) is an integer containing HTTP status code
982             (200 means OK, 4xx caller error, 5xx function error). Second element
983             (msg) is a string containing error message, or 'OK' if status is
984             200. Third element (result) is optional, the actual result. Fourth
985             element (meta) is called result metadata and is optional, a hash
986             that contains extra information.
987              
988             Return value: A hash of result (hash)
989              
990              
991             Will return status 200 if operation is successful and text is changed. The
992             result is a hash with the following keys: C<text> will contain the new text,
993             C<orig_payload> will contain the original payload before being removed/replaced,
994             C<orig_fragment> will contain the original fragment (or the text that matches
995             C<replace_pattern>).
996              
997             Will return status 304 if nothing is changed (i.e. if fragment with the
998             same payload that needs to be inserted already exists in the text).
999              
1000              
1001             =head2 list_fragments(%args) -> [status, msg, result, meta]
1002              
1003             List fragments in text.
1004              
1005             This function is not exported by default, but exportable.
1006              
1007             Arguments ('*' denotes required arguments):
1008              
1009             =over 4
1010              
1011             =item * B<comment_style> => I<str> (default: "shell")
1012              
1013             Comment style.
1014              
1015             =item * B<label> => I<str> (default: "FRAGMENT")
1016              
1017             Comment label.
1018              
1019             =item * B<text>* => I<str>
1020              
1021             The text which contain fragments.
1022              
1023             =back
1024              
1025             Returns an enveloped result (an array).
1026              
1027             First element (status) is an integer containing HTTP status code
1028             (200 means OK, 4xx caller error, 5xx function error). Second element
1029             (msg) is a string containing error message, or 'OK' if status is
1030             200. Third element (result) is optional, the actual result. Fourth
1031             element (meta) is called result metadata and is optional, a hash
1032             that contains extra information.
1033              
1034             Return value: List of fragments (array)
1035              
1036              
1037             Will return status 200 if operation is successful. Result will be an array of
1038             fragments, where each fragment is a hash containing these keys: C<raw> (string),
1039             C<payload> (string), C<attrs> (hash), C<id> (string, can also be found in
1040             attributes).
1041              
1042              
1043             =head2 set_fragment_attrs(%args) -> [status, msg, result, meta]
1044              
1045             Set/unset attributes of a fragment.
1046              
1047             If there are multiple occurences of the fragment with the same ID ,
1048              
1049             This function is not exported by default, but exportable.
1050              
1051             Arguments ('*' denotes required arguments):
1052              
1053             =over 4
1054              
1055             =item * B<attrs>* => I<hash>
1056              
1057             To delete an attribute in the fragment, you can set the value to undef.
1058              
1059             =item * B<comment_style> => I<str> (default: "shell")
1060              
1061             Comment style.
1062              
1063             =item * B<id>* => I<str>
1064              
1065             Fragment ID.
1066              
1067             =item * B<label> => I<str> (default: "FRAGMENT")
1068              
1069             Comment label.
1070              
1071             =item * B<text>* => I<str>
1072              
1073             The text which contain fragments.
1074              
1075             =back
1076              
1077             Returns an enveloped result (an array).
1078              
1079             First element (status) is an integer containing HTTP status code
1080             (200 means OK, 4xx caller error, 5xx function error). Second element
1081             (msg) is a string containing error message, or 'OK' if status is
1082             200. Third element (result) is optional, the actual result. Fourth
1083             element (meta) is called result metadata and is optional, a hash
1084             that contains extra information.
1085              
1086             Return value: New text and other data (array)
1087              
1088              
1089             Will return status 200 if fragment is found. Result will be a hash containing
1090             these keys: C<text> (string, the modified text), C<orig_attrs> (hash, the old
1091             attributes before being modified).
1092              
1093             Return 404 if fragment is not found.
1094              
1095             =head1 HOMEPAGE
1096              
1097             Please visit the project's homepage at L<https://metacpan.org/release/Text-Fragment>.
1098              
1099             =head1 SOURCE
1100              
1101             Source repository is at L<https://github.com/sharyanto/perl-Text-Fragment>.
1102              
1103             =head1 BUGS
1104              
1105             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Text-Fragment>
1106              
1107             When submitting a bug or request, please include a test-file or a
1108             patch to an existing test-file that illustrates the bug or desired
1109             feature.
1110              
1111             =head1 AUTHOR
1112              
1113             perlancar <perlancar@cpan.org>
1114              
1115             =head1 COPYRIGHT AND LICENSE
1116              
1117             This software is copyright (c) 2016 by perlancar@cpan.org.
1118              
1119             This is free software; you can redistribute it and/or modify it under
1120             the same terms as the Perl 5 programming language system itself.
1121              
1122             =cut