virginity 0.3.31
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/virginity.rb +6 -0
- data/lib/virginity/api_extensions.rb +87 -0
- data/lib/virginity/api_extensions/fields_to_json.rb +82 -0
- data/lib/virginity/api_extensions/fields_to_xml.rb +151 -0
- data/lib/virginity/bnf.rb +84 -0
- data/lib/virginity/dir_info.rb +93 -0
- data/lib/virginity/dir_info/content_line.rb +146 -0
- data/lib/virginity/dir_info/line_folding.rb +60 -0
- data/lib/virginity/dir_info/param.rb +208 -0
- data/lib/virginity/dir_info/query.rb +144 -0
- data/lib/virginity/encoding_decoding.rb +177 -0
- data/lib/virginity/encodings.rb +36 -0
- data/lib/virginity/fixes.rb +230 -0
- data/lib/virginity/vcard.rb +244 -0
- data/lib/virginity/vcard/base_field.rb +126 -0
- data/lib/virginity/vcard/categories.rb +57 -0
- data/lib/virginity/vcard/cleaning.rb +364 -0
- data/lib/virginity/vcard/field.rb +22 -0
- data/lib/virginity/vcard/field/params.rb +93 -0
- data/lib/virginity/vcard/field_values.rb +10 -0
- data/lib/virginity/vcard/field_values/binary.rb +22 -0
- data/lib/virginity/vcard/field_values/boolean.rb +14 -0
- data/lib/virginity/vcard/field_values/case_insensitive_value.rb +13 -0
- data/lib/virginity/vcard/field_values/date.rb +16 -0
- data/lib/virginity/vcard/field_values/integer.rb +15 -0
- data/lib/virginity/vcard/field_values/optional_structured_text.rb +35 -0
- data/lib/virginity/vcard/field_values/separated_text.rb +59 -0
- data/lib/virginity/vcard/field_values/structured_text.rb +71 -0
- data/lib/virginity/vcard/field_values/text.rb +23 -0
- data/lib/virginity/vcard/field_values/uri.rb +15 -0
- data/lib/virginity/vcard/fields.rb +284 -0
- data/lib/virginity/vcard/fields_osx.rb +95 -0
- data/lib/virginity/vcard/fields_soocial.rb +45 -0
- data/lib/virginity/vcard/name_handler.rb +151 -0
- data/lib/virginity/vcard/patching.rb +262 -0
- data/lib/virginity/vcard21.rb +2 -0
- data/lib/virginity/vcard21/base.rb +30 -0
- data/lib/virginity/vcard21/parser.rb +359 -0
- data/lib/virginity/vcard21/reader.rb +103 -0
- data/lib/virginity/vcard21/writer.rb +139 -0
- metadata +111 -0
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'virginity/encoding_decoding'
|
2
|
+
require 'virginity/dir_info/param'
|
3
|
+
|
4
|
+
module Virginity
|
5
|
+
|
6
|
+
# raised when merging will fail
|
7
|
+
class MergeError < Error; end
|
8
|
+
|
9
|
+
# rfc 2425 describes a content line as this:
|
10
|
+
# contentline = [group "."] name *(";" param) ":" value CRLF
|
11
|
+
# See also: DirectoryInformation
|
12
|
+
#
|
13
|
+
# A ContentLine is a tuple of one group (optional), a name, zero or more parameters and a value.
|
14
|
+
|
15
|
+
# Type names and parameter names are case insensitive (e.g., the type
|
16
|
+
# name "fn" is the same as "FN" and "Fn"). Parameter values MAY be case
|
17
|
+
# sensitive or case insensitive, depending on their definition.
|
18
|
+
class ContentLine
|
19
|
+
attr_accessor :group, :name, :value
|
20
|
+
attr_writer :params
|
21
|
+
alias_method :raw_value, :value
|
22
|
+
|
23
|
+
extend Encodings
|
24
|
+
include Encodings
|
25
|
+
|
26
|
+
# create a ContentLine by specifying all its parts
|
27
|
+
def initialize(name = "X-FOO", value = nil, params = [], group = nil, options = {})
|
28
|
+
@group = group
|
29
|
+
@name = name.to_s
|
30
|
+
@params = options[:no_deep_copy] ? (params) : Param.deep_copy(params)
|
31
|
+
@value = value.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
# decode a line
|
35
|
+
def self.parse(line = "X-FOO:bar")
|
36
|
+
group, name, params, value = line_parts(line.to_s)
|
37
|
+
# optimization: since params is deep_copied, constructing many objects and we know for certain that params can safely be used without copying, we put it in later.
|
38
|
+
new(name, value, params, group, :no_deep_copy => true)
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
alias_method :from_line, :parse
|
43
|
+
end
|
44
|
+
|
45
|
+
# the combination of two content lines
|
46
|
+
# This method will raise a MergeError if names, groups or values are conflicting
|
47
|
+
def self.merger(left, right)
|
48
|
+
raise MergeError, "group, #{left.group} != #{right.group}" unless left.group == right.group or left.group.nil? or left.group.nil?
|
49
|
+
raise MergeError, "name, #{left.name} != #{right.name}" unless left.has_name?(right.name)
|
50
|
+
raise MergeError, "value, #{left.raw_value} != #{right.raw_value}" unless left.raw_value == right.raw_value
|
51
|
+
ContentLine.new(left.name, left.raw_value, (left.params + right.params).uniq, left.group || right.group)
|
52
|
+
end
|
53
|
+
|
54
|
+
# combine with new values from another line.
|
55
|
+
# This method will raise a MergeError if names, groups or values are conflicting
|
56
|
+
def merge_with!(other)
|
57
|
+
raise MergeError, "group, #{group} != #{other.group}" unless group == other.group or group.nil? or other.group.nil?
|
58
|
+
raise MergeError, "name, #{name} != #{other.name}" unless has_name?(other.name)
|
59
|
+
raise MergeError, "value, #{raw_value} != #{other.raw_value}" unless raw_value == other.raw_value
|
60
|
+
self.group = group || other.group
|
61
|
+
self.params = (params + other.params).uniq
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
GROUP_DELIMITER = "."
|
66
|
+
COLON_CHAR = ":"
|
67
|
+
def encode #(options = {})
|
68
|
+
line = ""
|
69
|
+
line << group << GROUP_DELIMITER unless group.nil?
|
70
|
+
line << name << params_to_s << COLON_CHAR << raw_value
|
71
|
+
end
|
72
|
+
alias_method :to_s, :encode
|
73
|
+
|
74
|
+
def pretty_print(q)
|
75
|
+
q.text({:line => { group: @group, name: @name, params: params_to_s, value: @value }}.to_yaml)
|
76
|
+
end
|
77
|
+
|
78
|
+
def has_name?(name)
|
79
|
+
@name.casecmp(name) == 0
|
80
|
+
end
|
81
|
+
|
82
|
+
# if key is given, return only matching parameters
|
83
|
+
def params(key = nil)
|
84
|
+
if key.nil?
|
85
|
+
@params
|
86
|
+
else
|
87
|
+
@params.select { |param| param.has_key?(key) } # case insensitive by design!
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# convenience method to grab only the values of the parameters (without the keys)
|
92
|
+
def param_values(key = nil)
|
93
|
+
params(key).map { |param| param.value }
|
94
|
+
end
|
95
|
+
|
96
|
+
def ==(other)
|
97
|
+
group == other.group &&
|
98
|
+
has_name?(other.name) &&
|
99
|
+
params == other.params &&
|
100
|
+
raw_value == other.raw_value
|
101
|
+
end
|
102
|
+
|
103
|
+
def <=>(other)
|
104
|
+
str_diff(group, other.group) || str_diff(name, other.name) || (to_s <=> other.to_s)
|
105
|
+
end
|
106
|
+
|
107
|
+
def hash
|
108
|
+
[group, name, params, raw_value].hash
|
109
|
+
end
|
110
|
+
|
111
|
+
def eql?(other)
|
112
|
+
group == other.group &&
|
113
|
+
has_name?(other.name) &&
|
114
|
+
params == other.params &&
|
115
|
+
raw_value == other.raw_value
|
116
|
+
end
|
117
|
+
|
118
|
+
GROUP = /#{Bnf::NAME}\./
|
119
|
+
NAME = /#{Bnf::NAME}/
|
120
|
+
# decode a contentline, returns the four parts [group, name, params, value]
|
121
|
+
def self.line_parts(line)
|
122
|
+
scanner = StringScanner.new(line)
|
123
|
+
if group = scanner.scan(GROUP)
|
124
|
+
group.chomp!(".")
|
125
|
+
group
|
126
|
+
end
|
127
|
+
name = scanner.scan(NAME)
|
128
|
+
name.upcase! # FIXME: names should be case insensitive when compared... only when compared
|
129
|
+
[group, name, Param::scan_params(scanner), scanner.rest]
|
130
|
+
rescue InvalidEncoding
|
131
|
+
raise
|
132
|
+
rescue => e
|
133
|
+
raise InvalidEncoding, "#{scanner.string.inspect}, at pos #{scanner.pos} (original error: #{e})"
|
134
|
+
end
|
135
|
+
|
136
|
+
def params_to_s
|
137
|
+
Param::params_to_s(params)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
def str_diff(left, right)
|
142
|
+
diff = (left <=> right)
|
143
|
+
diff == 0 ? nil : diff
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
$KCODE = 'U' unless defined? Encoding::UTF_8
|
3
|
+
|
4
|
+
module Virginity
|
5
|
+
|
6
|
+
module LineFolding
|
7
|
+
LINE_ENDING = /\r\n|\n|\r/ # the order is important!
|
8
|
+
FOLD = /(#{LINE_ENDING})[\t\ ]/ # we accept unix-newlines and mac-newlines too (spec says only windows newlines, \r\n, are okay)
|
9
|
+
|
10
|
+
# 5.8.1. Line delimiting and folding.
|
11
|
+
# A logical line MAY be continued on the next physical line anywhere between two characters by inserting a CRLF immediately followed by a single white space character (space, ASCII decimal 32, or horizontal tab, ASCII decimal 9). At least one character must be present on the folded line. Any sequence of CRLF followed immediately by a single white space character is ignored (removed) when processing the content type.
|
12
|
+
def self.unfold(card)
|
13
|
+
card.gsub(FOLD, '')
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.unfold_and_split(string)
|
17
|
+
unfold(string).split(LINE_ENDING)
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO: option to encode with "\r\n" instead of "\n"?
|
21
|
+
# not multibyte-safe but very safe for ascii
|
22
|
+
def self.fold_ascii(card, width = 75)
|
23
|
+
return card unless width > 0
|
24
|
+
# binary should already be encoded to a width that is smaller than width
|
25
|
+
card.gsub(/.{#{width}}/, "\\0\n ") # "\\0" is the matched string
|
26
|
+
end
|
27
|
+
|
28
|
+
# utf-8 safe folding:
|
29
|
+
# TODO: I think this is a good candidate to be ported to C
|
30
|
+
def self.fold(card, width = 75)
|
31
|
+
return card unless width > 0
|
32
|
+
# binary fields should already be encoded to a width that is smaller than width
|
33
|
+
scanner = StringScanner.new(card)
|
34
|
+
folded = ""
|
35
|
+
line_pos = 0
|
36
|
+
while !scanner.eos?
|
37
|
+
char = scanner.getch
|
38
|
+
charsize = char.size
|
39
|
+
if line_pos + charsize > width
|
40
|
+
folded << "\n "
|
41
|
+
line_pos = 0
|
42
|
+
end
|
43
|
+
folded << char
|
44
|
+
char == "\n" ? line_pos = 0 : line_pos += charsize
|
45
|
+
end
|
46
|
+
folded
|
47
|
+
end
|
48
|
+
|
49
|
+
# Content lines SHOULD be folded to a maximum width of 75 octets, excluding the
|
50
|
+
# line break. Multi-octet characters MUST remain contiguous.
|
51
|
+
# So, we're doing it wrong, we should count octets... bytes.
|
52
|
+
|
53
|
+
# This is way faster than the method above and unicode-safe
|
54
|
+
# it is slightly different: it does not count bytes, it counts characters
|
55
|
+
def self.fold(card)
|
56
|
+
card.gsub(/.{75}(?=.)/, "\\0\n ") # "\\0" is the matched string
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
module Virginity
|
2
|
+
|
3
|
+
# A directory information parameter is basically a key-value pair.
|
4
|
+
# An instance of this class represents such a pair. It can deal with comparison and encoding. The class contains some methods to deal with lists of parameters
|
5
|
+
# Param keys are case insensitive
|
6
|
+
class Param
|
7
|
+
attr_reader :key
|
8
|
+
attr_accessor :value
|
9
|
+
|
10
|
+
def initialize(key, value)
|
11
|
+
self.key = key
|
12
|
+
@value = value.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
# convenience method, the same as calling Param.new("TYPE", value)
|
16
|
+
def self.type(value)
|
17
|
+
new('TYPE', value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.pref
|
21
|
+
new('TYPE', 'PREF')
|
22
|
+
end
|
23
|
+
|
24
|
+
# convenience method, the same as calling Param.new("CHARSET", value)
|
25
|
+
def self.charset(value)
|
26
|
+
new('CHARSET', value)
|
27
|
+
end
|
28
|
+
|
29
|
+
# convenience method, the same as calling Param.new("ENCODING", value)
|
30
|
+
def self.encoding(value)
|
31
|
+
new('ENCODING', value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# convenience method, the same as calling Param.new("ETAG", value)
|
35
|
+
def self.etag(value)
|
36
|
+
new('ETAG', value)
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"#{@key}=#{escaped_value}"
|
41
|
+
end
|
42
|
+
alias_method :inspect, :to_s
|
43
|
+
|
44
|
+
# param-name = x-name / iana-token
|
45
|
+
# iana-token = 1*(ALPHA / DIGIT / "-")
|
46
|
+
# ; identifier registered with IANA
|
47
|
+
PARAM_NAME_CHECK = /^((X|x)\-)?(\w|\-)+$/
|
48
|
+
def key=(key)
|
49
|
+
raise "Invalid param-key: #{key.inspect}" unless key =~ PARAM_NAME_CHECK
|
50
|
+
@key = key.upcase
|
51
|
+
end
|
52
|
+
|
53
|
+
def hash
|
54
|
+
[@key, @value].hash
|
55
|
+
end
|
56
|
+
|
57
|
+
def eql?(other)
|
58
|
+
other.is_a?(Param) && has_key?(other.key)&& @value == other.value
|
59
|
+
end
|
60
|
+
|
61
|
+
def has_key?(other_key)
|
62
|
+
@key.casecmp(other_key) == 0
|
63
|
+
end
|
64
|
+
|
65
|
+
# NB. Other doesn't necessarily have to be a Param
|
66
|
+
def ==(other)
|
67
|
+
has_key?(other.key) && @value == other.value
|
68
|
+
rescue
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
def <=>(other)
|
73
|
+
if has_key?(other.key)
|
74
|
+
@value <=> other.value
|
75
|
+
else
|
76
|
+
@key <=> other.key
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# A semi-colon in a property parameter value must be escaped with a Blackslash character.
|
81
|
+
# commas too I think, since these are used to separate the param-values
|
82
|
+
# param-value = ptext / quoted-string
|
83
|
+
# ptext = *SAFE-CHAR
|
84
|
+
# SAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E / NON-ASCII
|
85
|
+
# ; Any character except CTLs, DQUOTE, ";", ":", ","
|
86
|
+
# quoted-string = DQUOTE *QSAFE-CHAR DQUOTE
|
87
|
+
# QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII
|
88
|
+
# ; Any character except CTLs, DQUOTE
|
89
|
+
ESCAPE_CHARS = /\\|\;|\,|\"/
|
90
|
+
LF = "\n"
|
91
|
+
ESCAPED_LF = "\\n"
|
92
|
+
ESCAPE_CHARS_WITH_LF = /\\|\;|\,|\"|\n/
|
93
|
+
ESCAPE_HASH = {
|
94
|
+
'\\' => '\\\\',
|
95
|
+
';' => '\;',
|
96
|
+
',' => '\,',
|
97
|
+
'"' => '\"',
|
98
|
+
"\n" => '\n'
|
99
|
+
}
|
100
|
+
def escaped_value
|
101
|
+
if @value =~ ESCAPE_CHARS_WITH_LF
|
102
|
+
# put quotes around the escaped string
|
103
|
+
"\"#{@value.gsub(ESCAPE_CHARS) { |char| "\\#{char}" }.gsub(LF, ESCAPED_LF)}\""
|
104
|
+
# "\"#{@value.gsub(ESCAPE_CHARS_WITH_LF, ESCAPE_HASH)}\"" # ruby1.9
|
105
|
+
else
|
106
|
+
@value
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.decode_value(val)
|
111
|
+
if Bnf::QUOTED_STRING =~ val
|
112
|
+
EncodingDecoding::decode_text($2)
|
113
|
+
else
|
114
|
+
val
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# encodes an array of params
|
119
|
+
def self.simple_params_to_s(params)
|
120
|
+
return "" if params.empty?
|
121
|
+
";" + params.uniq.sort.join(";")
|
122
|
+
end
|
123
|
+
|
124
|
+
# encodes an array of params
|
125
|
+
# param = param-name "=" param-value *("," param-value)
|
126
|
+
def self.params_to_s(params)
|
127
|
+
return "" if params.empty?
|
128
|
+
s = []
|
129
|
+
params.map {|p| p.key }.uniq.sort!.each do |key|
|
130
|
+
values = params.select {|p| p.has_key? key }.map {|p| p.escaped_value }.uniq.sort!
|
131
|
+
s << "#{key}=#{values.join(",")}"
|
132
|
+
end
|
133
|
+
";#{s.join(";")}"
|
134
|
+
end
|
135
|
+
|
136
|
+
# encodes an array of params
|
137
|
+
def self.params_to_s(params)
|
138
|
+
return "" if params.empty?
|
139
|
+
s = ""
|
140
|
+
params.map { |p| p.key }.uniq.sort!.each do |key|
|
141
|
+
values = params.select {|p| p.has_key? key }.map {|p| p.escaped_value }.uniq.sort!
|
142
|
+
s << ";#{key}=#{values.join(",")}"
|
143
|
+
end
|
144
|
+
s
|
145
|
+
end
|
146
|
+
|
147
|
+
COLON = /:/
|
148
|
+
SEMICOLON = /;/
|
149
|
+
COMMA = /,/
|
150
|
+
EQUALS = /=/
|
151
|
+
KEY = /#{Bnf::NAME}/
|
152
|
+
PARAM_NAME = /((X|x)\-)?(\w|\-)+/
|
153
|
+
PARAM_VALUE = /#{Bnf::PVALUE}/
|
154
|
+
|
155
|
+
BASE64_OR_B = /^(BASE64)|(B)$/i
|
156
|
+
XSYNTHESIS_REF = /^X-Synthesis-Ref\d*$/i
|
157
|
+
|
158
|
+
# scans all params at the given position from a StringScanner including the pending colon
|
159
|
+
def self.scan_params(scanner)
|
160
|
+
unless scanner.skip(SEMICOLON)
|
161
|
+
scanner.skip(COLON)
|
162
|
+
return []
|
163
|
+
end
|
164
|
+
params = []
|
165
|
+
until scanner.skip(COLON) # a colon indicates the end of the paramlist
|
166
|
+
key = scanner.scan(PARAM_NAME)
|
167
|
+
unless scanner.skip(EQUALS)
|
168
|
+
# it's not a proper DirInfo param but nevertheless some companies (Apple) put vCard2.1 shorthand params without a key in they vCard3.0. Therefore we include support for these special cases.
|
169
|
+
case key
|
170
|
+
when Vcard21::QUOTED_PRINTABLE
|
171
|
+
params << Param.new('ENCODING', key)
|
172
|
+
when BASE64_OR_B
|
173
|
+
params << Param.new('ENCODING', 'B')
|
174
|
+
when XSYNTHESIS_REF
|
175
|
+
# we ignore this crap.
|
176
|
+
when *Vcard21::KNOWNTYPES
|
177
|
+
params << Param.new('TYPE', key)
|
178
|
+
else
|
179
|
+
raise InvalidEncoding, "encountered paramkey #{key.inspect} without a paramvalue"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
key.upcase!
|
183
|
+
begin
|
184
|
+
if value = scanner.scan(PARAM_VALUE)
|
185
|
+
params << Param.new(key, Param::decode_value(value))
|
186
|
+
end
|
187
|
+
break if scanner.skip(SEMICOLON) # after a semicolon, we expect key=(value)+
|
188
|
+
end until scanner.skip(COMMA).nil? # a comma indicates another values (TYPE=HOME,WORK)
|
189
|
+
end
|
190
|
+
params
|
191
|
+
rescue InvalidEncoding
|
192
|
+
raise
|
193
|
+
rescue => e
|
194
|
+
raise InvalidEncoding, "#{scanner.string.inspect} at character #{scanner.pos}\noriginal error: #{e}"
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.params_from_string(string)
|
198
|
+
scan_params(StringScanner.new(string))
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.deep_copy(paramlist)
|
202
|
+
return [] if paramlist.nil? or paramlist.empty?
|
203
|
+
# params_from_string(simple_params_to_s(paramlist)) # slower
|
204
|
+
Marshal::load(Marshal::dump(paramlist)) # faster
|
205
|
+
end
|
206
|
+
|
207
|
+
end # class Param
|
208
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Virginity
|
2
|
+
|
3
|
+
# Helper methods to perform queries on lines in a DirectoryInformation instance
|
4
|
+
#
|
5
|
+
# query format: <tt>[group.][name][:value]</tt>
|
6
|
+
#
|
7
|
+
# examples:
|
8
|
+
# * "FN" searches for FN-fields
|
9
|
+
# * "FN:John Smith" searches for an FN-field with the value "John Smith"
|
10
|
+
# * ":John Smith" searches for any field with the value "John Smith"
|
11
|
+
# * "item1.:John Smith" searches for any field with the value "John Smith" and group item1
|
12
|
+
module Query
|
13
|
+
|
14
|
+
# if a query cannot be parsed an InvalidQuery is raised
|
15
|
+
# the query-decoder expects correct input, it will not attempt to find errors or even correct them for you
|
16
|
+
class InvalidQuery < Error; end
|
17
|
+
|
18
|
+
GROUP = /#{Bnf::NAME}\./
|
19
|
+
NAME = /#{Bnf::NAME}/
|
20
|
+
SEMICOLON = /\;/
|
21
|
+
COLON = /\:/
|
22
|
+
def self.decode_query(query)
|
23
|
+
scanner = StringScanner.new(query)
|
24
|
+
# does the query start with a name that ends in a dot?
|
25
|
+
if group = scanner.scan(GROUP)
|
26
|
+
group.chomp!(".")
|
27
|
+
end
|
28
|
+
name = scanner.scan(NAME) # could be nil
|
29
|
+
params = params(scanner)
|
30
|
+
value = nil
|
31
|
+
if scanner.skip(COLON)
|
32
|
+
value = scanner.rest
|
33
|
+
end
|
34
|
+
[group, name, params, value]
|
35
|
+
end
|
36
|
+
|
37
|
+
COMMA = /,/
|
38
|
+
EQUALS = /=/
|
39
|
+
KEY = /#{Bnf::NAME}/
|
40
|
+
PARAM_NAME = /((X|x)\-)?(\w|\-)+/
|
41
|
+
PARAM_VALUE = /#{Bnf::PVALUE}/
|
42
|
+
def self.params(scanner)
|
43
|
+
return nil unless scanner.skip(SEMICOLON)
|
44
|
+
params = []
|
45
|
+
until scanner.check(COLON) or scanner.eos? # <--- check of end of string! and *check* for colon
|
46
|
+
key = scanner.scan(PARAM_NAME)
|
47
|
+
raise InvalidQuery unless scanner.skip(EQUALS)
|
48
|
+
begin
|
49
|
+
if value = scanner.scan(PARAM_VALUE)
|
50
|
+
params << Param.new(key, Param::decode_value(value))
|
51
|
+
end
|
52
|
+
break if scanner.skip(SEMICOLON) # after a semicolon, we expect key=(value)+
|
53
|
+
end until scanner.skip(COMMA).nil? # a comma indicates another value (TYPE=HOME,WORK)
|
54
|
+
end
|
55
|
+
params
|
56
|
+
end
|
57
|
+
|
58
|
+
def query(query = "")
|
59
|
+
group, name, params, value = Query::decode_query(query)
|
60
|
+
lines.select do |line|
|
61
|
+
# to_s is used here to match a nil-group to the empty-group-query: "."
|
62
|
+
(group.nil? || group == line.group.to_s) &&
|
63
|
+
(params.nil? || params.all? { |p| line.params.include? p }) &&
|
64
|
+
(name.nil? || line.has_name?(name)) &&
|
65
|
+
(value.nil? || value == line.raw_value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# return the first match for query q. By definition this is equivalent to query(q).first but it is faster.
|
70
|
+
def first_match(query = "")
|
71
|
+
group, name, params, value = Query::decode_query(query)
|
72
|
+
lines.detect do |line|
|
73
|
+
# to_s is used here to match a nil-group to the empty-group-query: "."
|
74
|
+
(group.nil? || group == line.group.to_s) &&
|
75
|
+
(params.nil? || params.all? { |p| line.params.include?(p) }) &&
|
76
|
+
(name.nil? || line.has_name?(name)) &&
|
77
|
+
(value.nil? || value == line.raw_value)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def lines_with_name(name = "")
|
82
|
+
lines.select do |line|
|
83
|
+
line.has_name?(name)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def line_matches_query?(line, q, v)
|
88
|
+
raise ArgumentError, "query cannot be nil { #{q.inspect} => #{v.inspect} }?" if v.nil?
|
89
|
+
case q
|
90
|
+
when :name
|
91
|
+
line.has_name?(v)
|
92
|
+
when :raw_value
|
93
|
+
v == line.raw_value
|
94
|
+
when :text
|
95
|
+
return false unless line.respond_to? :text
|
96
|
+
v == line.text
|
97
|
+
when :sha1
|
98
|
+
return false unless line.respond_to? :sha1
|
99
|
+
v == line.sha1
|
100
|
+
when :group
|
101
|
+
v == line.group
|
102
|
+
when :values
|
103
|
+
raise ArgumentError, "expected an array of values { #{q.inspect} => #{v.inspect} }?" unless v.is_a? Array
|
104
|
+
if line.respond_to? :values
|
105
|
+
if line.respond_to? :components # stuctured text, values are ordered and the lenght of the array is guaranteed to be correct.
|
106
|
+
line.values == v
|
107
|
+
else
|
108
|
+
(line.values - v).empty?
|
109
|
+
end
|
110
|
+
else
|
111
|
+
false
|
112
|
+
end
|
113
|
+
when :has_param
|
114
|
+
# true if one param matches the array we feed it
|
115
|
+
line.params.include? Param.new(v.first, v.last)
|
116
|
+
when :has_param_with_key
|
117
|
+
# true if one param matches the array we feed it
|
118
|
+
line.params.any? { |p| p.has_key?(v) }
|
119
|
+
when :has_type
|
120
|
+
# true if one param matches the array we feed it
|
121
|
+
line.params.include? Param.new('TYPE', v)
|
122
|
+
when :params
|
123
|
+
# all params match v, and no param is not matching
|
124
|
+
raise NotImplementedError
|
125
|
+
else
|
126
|
+
raise ArgumentError, "what do you expect me to do with { #{q.inspect} => #{v.inspect} }?"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def where(query = {})
|
131
|
+
lines.select do |line|
|
132
|
+
query.all? { |q,v| line_matches_query?(line, q, v) }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def find_first(query = {})
|
137
|
+
lines.detect do |line|
|
138
|
+
query.all? { |q,v| line_matches_query?(line, q, v) }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
alias_method(:/, :query) #/# kate's syntax highlighter is (slightly) broken so I put a slash here
|
143
|
+
end
|
144
|
+
end
|