File Coverage

blib/lib/String/TagString.pm
Criterion Covered Total %
statement 53 53 100.0
branch 29 32 90.6
condition 13 18 72.2
subroutine 8 8 100.0
pod 2 2 100.0
total 105 113 92.9


line stmt bran cond sub pod time code
1 1     1   55496 use warnings;
  1         11  
  1         29  
2 1     1   5 use strict;
  1         2  
  1         83  
3             package String::TagString 0.006;
4             # ABSTRACT: parse and emit tag strings (including tags with values)
5              
6             #pod =head1 SYNOPSIS
7             #pod
8             #pod use String::TagString;
9             #pod
10             #pod # Parse a string into a set of tags:
11             #pod my $tags = String::TagString->tags_from_string($string);
12             #pod
13             #pod # Represent a set of tags as a string:
14             #pod my $string = String::TagString->string_from_tags($tags);
15             #pod
16             #pod =head1 DESCRIPTION
17             #pod
18             #pod String::TagString enables Web 2.0 synergy by deconstructing and synthesizing
19             #pod folksonomic nomenclature into structured dynamic programming ontologies.
20             #pod
21             #pod Also, it parses strings of "tags" into hashrefs, so you can tag whatever junk
22             #pod you want with strings.
23             #pod
24             #pod A set of tags is an unordered set of simple strings, each possibly associated
25             #pod with a simple string value. This library parses strings of these tags into
26             #pod hashrefs, and turns hashrefs (or arrayrefs) back into these strings.
27             #pod
28             #pod This string:
29             #pod
30             #pod my $string = q{ beef cheese: peppers:hot };
31             #pod
32             #pod Turns into this hashref:
33             #pod
34             #pod my $tags = {
35             #pod beef => undef,
36             #pod cheese => '',
37             #pod peppers => 'hot',
38             #pod };
39             #pod
40             #pod That hashref, of course, would turn back into the same string -- although
41             #pod sorting is not guaranteed.
42             #pod
43             #pod =head2 Tag String Syntax
44             #pod
45             #pod Tag strings are space-separated tags. Tag syntax may change slightly in the
46             #pod future, so don't get too attached to any specific quirk, but basically:
47             #pod
48             #pod A tag is a name, then optionally a colon and value.
49             #pod
50             #pod Tag names can contains letters, numbers, dots underscores, and dashes. They
51             #pod can't start with a dash, but they can start with an at sign.
52             #pod
53             #pod A value is similar, but cannot start with an at sign.
54             #pod
55             #pod Alternately, either a tag or a value can be almost anything if it enclosed in
56             #pod double quotes. (Internal double quotes can be escaped with a backslash.)
57             #pod
58             #pod =method tags_from_string
59             #pod
60             #pod my $tag_hashref = String::TagString->tags_from_string($tag_string);
61             #pod
62             #pod This will either return a hashref of tags, as described above, or raise an
63             #pod exception. It will raise an exception if the string can't be interpreted, or
64             #pod if a tag appears multiple times with conflicting definitions, like in these
65             #pod examples:
66             #pod
67             #pod foo foo:
68             #pod
69             #pod foo:1 foo:2
70             #pod
71             #pod =cut
72              
73 1     1   499 sub _raw_tag_name_re { qr{@?(?:\pL|[\d_.*])(?:\pL|[-\d_.*])*} }
  1     36   12  
  1         12  
  36         82  
74 24     24   44 sub _raw_tag_value_re { qr{(?:\pL|[-\d_.*])*} }
75              
76             sub tags_from_string {
77 19     19 1 7565 my ($class, $tagstring) = @_;
78              
79 19 100 66     106 return {} unless $tagstring and $tagstring =~ /\S/;
80              
81             # remove leading and trailing spaces
82 18         59 $tagstring =~ s/\A\s*//;
83 18         36 $tagstring =~ s/\s*\a//;
84              
85 18         48 my $quoted_re = qr{ "( (?:\\\\|\\"|\\[^\\"]|[^\\"])+ )" }x;
86 18         34 my $raw_lhs_re = $class->_raw_tag_name_re;
87 18         34 my $raw_rhs_re = $class->_raw_tag_value_re;
88              
89 18         119 my $tag_re = qr{
90             (?: ( $raw_lhs_re | $quoted_re )) # $1 = whole match; $2 = quoted part
91             ( : # $3 = entire value, with :
92             ( $raw_rhs_re | $quoted_re )? # $4 = whole match; $5 = quoted part
93             )?
94             (?:\+|\s+|\z) # end-of-string or some space or a +
95             }x;
96              
97 18         402 my %tag;
98             my $pos;
99 18         127 while ($tagstring =~ m{\G$tag_re}g) {
100 34         336 $pos = pos $tagstring;
101 34 100       80 my $tag = defined $2 ? $2 : $1;
102 34 100       59 my $value = defined $5 ? $5 : $4;
103 34 50 66     84 $value = '' if ! defined $value and defined $3;
104 34 100       51 $value =~ s/\\"/"/g if defined $value;
105              
106 34 100       58 if (exists $tag{ $tag }) {
107 4 100       8 if (defined $tag{ $tag }) {
108             die "invalid tagstring: conflicting entries for $tag"
109 2 100 66     17 if (! defined $value) or $value ne $tag{ $tag };
110             } else {
111 2 100       12 die "invalid tagstring: conflicting entries for $tag"
112             if defined $value;
113             }
114             }
115              
116 32         177 $tag{ $tag } = $value;
117             }
118              
119 16 100 100     60 die "invalid tagstring" unless defined $pos and $pos == length $tagstring;
120              
121 13         54 return \%tag;
122             }
123              
124             #pod =method string_from_tags
125             #pod
126             #pod my $string = String::TagString->string_from_tags( $tag_set );
127             #pod
128             #pod This method returns a string representing the given tags. C<$tag_set> may be
129             #pod either a hashref or arrayref. An arrayref is treated like a hashref in which
130             #pod every value is undef.
131             #pod
132             #pod Tag names and values will only be quoted if needed.
133             #pod
134             #pod =cut
135              
136             sub _qs {
137 24     24   42 my ($self, $type, $str) = @_;
138 24         38 my $method = "_raw_tag_$type\_re";
139 24         48 my $re = $self->$method;
140 24 100       259 return $str if $str =~ m{\A$re\z};
141 4         489 $str =~ s/\\/\\\\/g;
142 4         10 $str =~ s/"/\\"/g;
143 4         18 return qq{"$str"};
144             }
145              
146             sub string_from_tags {
147 13     13 1 5791 my ($class, $tags) = @_;
148              
149 13 50       36 return "" unless defined $tags;
150              
151 13 50 66     47 Carp::carp("tagstring must be a hash or array reference")
      66        
152             unless (ref $tags) and ((ref $tags eq 'HASH') or (ref $tags eq 'ARRAY'));
153              
154 13 100       37 if (ref $tags eq 'ARRAY') {
155             Carp::croak("undefined tag name in array reference")
156 3 100       7 if grep { ! defined } @$tags;
  2         173  
157              
158 2         4 $tags = { map { $_ => undef } @$tags };
  1         3  
159             }
160              
161 12         16 my @tags;
162 12         41 for my $name (sort keys %$tags) {
163 18         265 my $value = $tags->{$name};
164 18 100       31 push @tags, join q{:},
165             $class->_qs(name => $name),
166             (defined $value ? $class->_qs(value => $value) : ());
167             }
168              
169 12         1168 return join q{ }, @tags;
170             }
171              
172             1;
173              
174             __END__