plain_text 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +51 -0
- data/ChangeLog +5 -0
- data/Makefile +23 -0
- data/README.en.rdoc +172 -0
- data/Rakefile +9 -0
- data/bin/countchar +89 -0
- data/lib/plain_text/parse_rule.rb +474 -0
- data/lib/plain_text/part/boundary.rb +44 -0
- data/lib/plain_text/part/paragraph.rb +35 -0
- data/lib/plain_text/part.rb +973 -0
- data/lib/plain_text/split.rb +103 -0
- data/lib/plain_text/util.rb +104 -0
- data/lib/plain_text.rb +839 -0
- data/plain_text.gemspec +49 -0
- data/test/test_plain_text.rb +280 -0
- data/test/test_plain_text_parse_rule.rb +146 -0
- data/test/test_plain_text_part.rb +353 -0
- data/test/test_plain_text_split.rb +78 -0
- metadata +72 -0
@@ -0,0 +1,474 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module PlainText
|
4
|
+
|
5
|
+
# Class to describe rules to parse a String (and Array of them)
|
6
|
+
#
|
7
|
+
# An instance (say, +pr+) of this class describes how a String (or Array of them)
|
8
|
+
# is parsed to a structure, that is, an Array of String or maybe
|
9
|
+
# {PlainText::Part}, {PlainText::Part::Paragraph}, {PlainText::Part::Boundary}.
|
10
|
+
# Once +pr+ is created, a String +str+ is parsed as
|
11
|
+
# ary = pr.apply(str)
|
12
|
+
# which returns an Array (referred to as +ary+ hereafter).
|
13
|
+
#
|
14
|
+
# The returned array +ary+ may contain Strings at the basic level. In that case,
|
15
|
+
# any even elements are semantically {PlainText::Part::Boundary} and any odd elements are
|
16
|
+
# semantically {PlainText::Part::Paragraph} or {PlainText::Part}, which can be further parsed
|
17
|
+
# in the later processing.
|
18
|
+
#
|
19
|
+
# Alternatively, the returned array +ary+ may contain
|
20
|
+
# {PlainText::Part::Paragraph}, {PlainText::Part::Boundary}, or even {PlainText::Part},
|
21
|
+
# depending how the instance +pr+ is constructed.
|
22
|
+
#
|
23
|
+
# An instance +pr+ consists of an array of the rules (which can be retrieved by {#rules});
|
24
|
+
# each rule of it is either a Proc instance or Regexp.
|
25
|
+
# The rule is applied to either String (for the first-time application only)
|
26
|
+
# or Array (for any subsequent applications), the latter of which is
|
27
|
+
# (though it does not have to be) the result of the previous applications, and an Array is returned.
|
28
|
+
# Elements of {#rules} (particularly common for for {#rules}[ 0 ]) can be Regexp,
|
29
|
+
# in which case either the given String or every element of an even index (starting from 0;
|
30
|
+
# they all are semantically Paragraphs) of the given Array is String#split as defined in the rule
|
31
|
+
# to return an Array. This manipulation with String#split in general increases the number of the elements
|
32
|
+
# (Array#size) if an Array is given as the argument. For example, suppose the given Array has initially two elements,
|
33
|
+
# and suppose String#split is applied to the first element (only), and it may create 5 elements. Then, the resultant
|
34
|
+
# number of elements of the returned array is 6.
|
35
|
+
#
|
36
|
+
# For the second or later application, the element, Proc, must assume the argument is an Array
|
37
|
+
# (of String or even {PlainText}::SOMETHING objects) and process them accordingly.
|
38
|
+
#
|
39
|
+
# For example, the predefined constant {PlainText::ParseRule::RuleConsecutiveLbs}
|
40
|
+
# is one of the instances and it splits a String based on any consecutive linebreaks
|
41
|
+
# (it is typical to regard paragraphs as being separated by consecutive linebreaks).
|
42
|
+
# An example is like this:
|
43
|
+
#
|
44
|
+
# pr.rules[0] # => The rule is: PlainText::ParseRule::RuleConsecutiveLbs.rules[0]
|
45
|
+
# # Once applied, the returned Array is like
|
46
|
+
# # ["My story\n======\nHere is my report.",
|
47
|
+
# # "\n\n", "abc", "\n\n", "xyz"]
|
48
|
+
# pr.rules[1] # => /(\n={4,}\n)/
|
49
|
+
# # Once applied, the returned Array is like
|
50
|
+
# # ["My story", "\n======\n", "Here is my report.",
|
51
|
+
# # "\n\n", "abc", "\n\n", "xyz"]
|
52
|
+
#
|
53
|
+
# Or another example may be like this:
|
54
|
+
#
|
55
|
+
# pr.rules[0] # => The rule: PlainText::ParseRule::RuleConsecutiveLbs.rules[0]
|
56
|
+
# # Once applied, the returned Array is like
|
57
|
+
# # ["# Breaking! #\nBy Mary Smith\n======\nHere is my report.",
|
58
|
+
# # "\n\n", "abc", "\n\n", "xyz"]
|
59
|
+
# pr.rules[1] # => The rule: For the first element of the input argument (Array), if it has one "\n======\n",
|
60
|
+
# # it is regarded as a (the first) boundary, and the text before
|
61
|
+
# # is regarded as {PlainText::Part}. The returned Array is like
|
62
|
+
# # [Part("# Breaking! #\nBy Mary Smith"),
|
63
|
+
# # Boundary("\n======\n"),
|
64
|
+
# # Paragraph("Here is my report."),
|
65
|
+
# # "\n\n", "abc", "\n\n", "xyz"]
|
66
|
+
# pr.rules[2] # => The rule: For the first element of the input argument (Array), if it satisfies /# (.+) #/,
|
67
|
+
# # it is regarded as a title of a header. The returned Array is like
|
68
|
+
# # [Part::Header(Paragraph(""), Boundary("# "), Paragraph::Title("Breaking!"), Boundary(" #\n")),
|
69
|
+
# # Boundary(""),
|
70
|
+
# # Paragraph("By Mary Smith"),
|
71
|
+
# # Boundary("\n======\n"),
|
72
|
+
# # Paragraph("Here is my report."),
|
73
|
+
# # "\n\n", "abc", "\n\n", "xyz"]
|
74
|
+
#
|
75
|
+
# With this, a {PlainText::Part} instance can be created like:
|
76
|
+
#
|
77
|
+
# pt1 = PlainText::Part.parse(str, rule: pr)
|
78
|
+
#
|
79
|
+
# Then,
|
80
|
+
#
|
81
|
+
# pt1.parts[0].parts[1] # => Paragraph::Title("Breaking!")
|
82
|
+
# pt1.boundaries[1] # => Boundary("\n======\n")
|
83
|
+
#
|
84
|
+
# @author Masa Sakano (Wise Babel Ltd)
|
85
|
+
#
|
86
|
+
class ParseRule
|
87
|
+
|
88
|
+
# Main Array of rules (Proc or Regexp). Do not delete or add the contents, as it would have a knock-on effect, especially with {#names}!
|
89
|
+
# Use {#rule_at} to get a rule for the index/key.
|
90
|
+
# The private method {#rule_at}(-1) is the same as {#rules}[-1],
|
91
|
+
# but is more versatile and can be called like +#rules_at(:my_rule1, :my_rule2)+.
|
92
|
+
attr_reader :rules
|
93
|
+
|
94
|
+
# User-specified human-readable names Array, corresponding to each element of {#rules}.
|
95
|
+
# The elements of this array are either String or nil, though it can be referred to as,
|
96
|
+
# or set with {#set_name_at}, with Symbol. In other words, an element of {#rules}
|
97
|
+
# can be specified with a human-readable name, if set, as well as its index.
|
98
|
+
# Use {#rule_at} to get a rule for the index/key.
|
99
|
+
attr_reader :names
|
100
|
+
|
101
|
+
# Constructor
|
102
|
+
#
|
103
|
+
# The main argument is a single or an Array of Proc or Regexp.
|
104
|
+
# Alternatively, a block can be given.
|
105
|
+
# If Regexp(s) is given, it should include grouping
|
106
|
+
# (to enclose the entire Regexp usually). If not, grouping is added forcibly.
|
107
|
+
#
|
108
|
+
# Note that the method (private method {#add_grouping}) wrongly recognizes patterns like +/[(x]/+ to contain grouping.
|
109
|
+
# Also, it does not raise warning when more than one grouping is defined.
|
110
|
+
# In fact, multiple groupings might be useful in some cases, such as,
|
111
|
+
# /(\n{2,})([^\n]*\S)([[:blank:]]*={2,}\n{2,})/
|
112
|
+
# would produce, when applied, a series of
|
113
|
+
# [Paragraph, Boundary("\n\n"), Paragraph::Title, Boundary("==\n\n")]
|
114
|
+
# Just make sure the number of groupings is an odd number, though.
|
115
|
+
#
|
116
|
+
# Optionally, when a non-Array argument or block is given, a name can be specified as the human-readable name for the rule.
|
117
|
+
#
|
118
|
+
# @option rule [ParseRule, Array, Regexp, Proc]
|
119
|
+
# @param name: [String, Symbol]
|
120
|
+
#
|
121
|
+
# @yield [inprm] Block to register.
|
122
|
+
# @yieldparam [String, Array<Part, Paragraph, Boundary>, Part] inprm Input String/Part/Array to apply the rule to.
|
123
|
+
# @yieldreturn [Array]
|
124
|
+
def initialize(rule=nil, name: nil, &rule_block)
|
125
|
+
if defined?(rule.rules) && defined?(rule.names)
|
126
|
+
# ParseRule given
|
127
|
+
@rules = rule.rules.clone.map{|i| i.clone rescue i} # Deep copy
|
128
|
+
@names = rule.names.clone.map{|i| i.clone rescue i} # Deep copy
|
129
|
+
return
|
130
|
+
end
|
131
|
+
|
132
|
+
if defined? rule.to_ary
|
133
|
+
# Array given
|
134
|
+
@rules = rule
|
135
|
+
@names = Array.new(@rules.size)
|
136
|
+
return
|
137
|
+
end
|
138
|
+
|
139
|
+
@rules = []
|
140
|
+
@names = []
|
141
|
+
push(rule, name: name, &rule_block)
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
# If no grouping is specified in Regexp, this method encloses it with '()'
|
146
|
+
#
|
147
|
+
# Because otherwise Boundaries would not be recognized.
|
148
|
+
#
|
149
|
+
# Note that this wrongly recognizes patterns like +/[(x]/+ to contain grouping.
|
150
|
+
# Also, this does not raise warning when more than one grouping is defined.
|
151
|
+
# In fact, multiple groupings might be useful in some cases, such as,
|
152
|
+
# /(\n{2,})([^\n]*\S)([[:blank:]]*={2,}\n{2,})/
|
153
|
+
# would produce, when applied, a series of
|
154
|
+
# [Paragraph, Boundary("\n\n"), Paragraph::Title, Boundary("==\n\n")]
|
155
|
+
# Just make sure the number of groupings is an odd number, though.
|
156
|
+
#
|
157
|
+
# @param rule_re [Regexp]
|
158
|
+
# @return [Regexp]
|
159
|
+
# @see PlainText::Split.add_grouping
|
160
|
+
def add_grouping(rule_re)
|
161
|
+
re_src = rule_re.source
|
162
|
+
return rule_re if /(?<!\\)(?:(\\\\)*)\((?!\?:)/ =~ re_src
|
163
|
+
|
164
|
+
# No "explicit" grouping is specified. Hence adds it.
|
165
|
+
Regexp.new '('+re_src+')', rule_re.options
|
166
|
+
end
|
167
|
+
private :add_grouping
|
168
|
+
|
169
|
+
|
170
|
+
alias_method :clone_original_b4_parse_rule?, :clone if !method_defined? :clone_original_b4_parse_rule?
|
171
|
+
|
172
|
+
# Deeper clone
|
173
|
+
#
|
174
|
+
# Without this, if @rules or @names are modified in a cloned instance,
|
175
|
+
# even the original is affected.
|
176
|
+
#
|
177
|
+
# @return the same as self
|
178
|
+
def clone
|
179
|
+
ret = clone_original_b4_parse_rule?
|
180
|
+
begin
|
181
|
+
ret.instance_eval{ @rules = rules.clone }
|
182
|
+
ret.instance_eval{ @names = names.clone }
|
183
|
+
rescue FrozenError
|
184
|
+
warn "Instances in the original remain frozen after clone."
|
185
|
+
end
|
186
|
+
ret
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
alias_method :dup_original_b4_parse_rule?, :dup if !method_defined? :dup_original_b4_parse_rule?
|
191
|
+
|
192
|
+
# Deeper dup
|
193
|
+
#
|
194
|
+
# Without this, if @rules or @names are modified in a dupped instance,
|
195
|
+
# even the original is affected.
|
196
|
+
#
|
197
|
+
# @return the same as self
|
198
|
+
def dup
|
199
|
+
ret = dup_original_b4_parse_rule?
|
200
|
+
ret.instance_eval{ @rules = rules.dup }
|
201
|
+
ret.instance_eval{ @names = names.dup }
|
202
|
+
ret
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
# Add a rule(s)
|
207
|
+
#
|
208
|
+
# If Regexp is given, it should include grouping (to enclose the entire Regexp usually). If not, grouping is added forcibly.
|
209
|
+
# Or, Proc or block can be given.
|
210
|
+
# Consecutive rules can be given. Note if a rule(s) is given, a block is ignored even if present.
|
211
|
+
#
|
212
|
+
# Any given rules, except the very first one, where the Proc argument is a String, should assume the Proc argument is an Array.
|
213
|
+
# If Regexp is given for the second or later one, it will raise an Exception when {#apply}-ed.
|
214
|
+
#
|
215
|
+
# Optionally, providing non-Array argument or block is given, a name can be specified as the human-readable name for the rule.
|
216
|
+
#
|
217
|
+
# @option *rule [Regexp, Proc]
|
218
|
+
# @param name: [String, Symbol, NilClass, Array<String, Symbol, NilClass>] Array is not supported, yet.
|
219
|
+
# @return [self]
|
220
|
+
#
|
221
|
+
# @yield [inprm] Block to register.
|
222
|
+
# @yieldparam [String, Array<Part, Paragraph, Boundary>, Part] inprm Input String/Part/Array to apply the rule to.
|
223
|
+
# @yieldreturn [Array]
|
224
|
+
def push(*rule, name: nil, &rule_block)
|
225
|
+
#if rule.size > 1
|
226
|
+
# rule.each do |each_r|
|
227
|
+
# push each_r, rule_block
|
228
|
+
# end
|
229
|
+
# return self
|
230
|
+
#end
|
231
|
+
|
232
|
+
push_rule_core(*rule, &rule_block)
|
233
|
+
set_name_at(name, -1) if !rules.empty?
|
234
|
+
# rulesize = ((0 != rule.size) ? rule.size : (block_given? ? 1 : 0))
|
235
|
+
### print "DEBUG-p: rulesize=#{rulesize}\n"
|
236
|
+
# arnames = (name ? [name].flatten : [])
|
237
|
+
# ((-rulesize)..-1).each_with_index do |i_rule, i_given|
|
238
|
+
# set_name_at(arnames[i_given], i_rule)
|
239
|
+
# end if !rule.empty?
|
240
|
+
self
|
241
|
+
end
|
242
|
+
|
243
|
+
# @option *rule [Regexp, Proc]
|
244
|
+
# @return [self]
|
245
|
+
#
|
246
|
+
# @yield [inprm] Block to register.
|
247
|
+
# @yieldparam [String, Array<Part, Paragraph, Boundary>, Part] inprm Input String/Part/Array to apply the rule to.
|
248
|
+
# @yieldreturn [Array]
|
249
|
+
def push_rule_core(*rule, &rule_block)
|
250
|
+
# If rule is given, it is guaranteed to be a single component.
|
251
|
+
rule0 = rule[0]
|
252
|
+
if rule0
|
253
|
+
raise ArgumentError, "Argument and block are not allowed to be given simultaneously." if block_given?
|
254
|
+
if defined?(rule0.source) && defined?(rule0.options)
|
255
|
+
# Regexp given
|
256
|
+
@rules.push add_grouping(rule0)
|
257
|
+
return self
|
258
|
+
end
|
259
|
+
|
260
|
+
if defined? rule0.lambda?
|
261
|
+
# Proc given
|
262
|
+
@rules.push rule0
|
263
|
+
return self
|
264
|
+
end
|
265
|
+
|
266
|
+
raise ArgumentError, "Invalid rule is given."
|
267
|
+
end
|
268
|
+
|
269
|
+
raise ArgumentError, "Neither an argument nor block is given." if !block_given?
|
270
|
+
|
271
|
+
@rules.push rule_block
|
272
|
+
self
|
273
|
+
end
|
274
|
+
private :push_rule_core
|
275
|
+
|
276
|
+
# Set (or reset) a human-readable name for {#rules} at a specified index
|
277
|
+
#
|
278
|
+
# @param name [NilClass, #to_s] nil to reset or a human-readable name, usually either String or Symbol
|
279
|
+
# @param index [Integer] Index for {#rules}. A negative index is allowed.
|
280
|
+
# @return [Integer] Non-negative index where name is set; i.e., if index=-1 is specified for {#rules} with a size of 3, the returned value is 2 (the last index of it).
|
281
|
+
def set_name_at(name, index_rules)
|
282
|
+
index = PlainText::Util.positive_array_index_checked(index_rules, @rules, accept_too_big: false, varname: 'rules')
|
283
|
+
if !name
|
284
|
+
@names[index] = nil
|
285
|
+
return index
|
286
|
+
end
|
287
|
+
ns = name.to_s
|
288
|
+
index_exist = @names.find_index(ns)
|
289
|
+
raise "Name #{ns} is already used for the index #{index}" if index_exist && (index_exist != index)
|
290
|
+
@names[index] = ns
|
291
|
+
index
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
# Get a rule for the specified index or human-readable key
|
296
|
+
#
|
297
|
+
# @param key [Integer, String, Symbol] Key for @rules
|
298
|
+
# @return [Proc, Regexp, NilClass] nil if the specified rule is not found.
|
299
|
+
def rule_at(key)
|
300
|
+
begin
|
301
|
+
( defined?(key.to_int) ? @rules[key.to_int] : @rules[@names.find_index(key.to_s)] )
|
302
|
+
rescue TypeError # no implicit conversion from nil to integer
|
303
|
+
nil
|
304
|
+
# raise TypeError, "Specified key (#{key.inspect}) is not found for the rules among the registered names=#{@names.inspect}"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
|
309
|
+
# Get an array of rules for the specified indices or human-readable keys
|
310
|
+
#
|
311
|
+
# If an Array or sequence of arguments is given, it can be a combination of Integer and String/Symbol,
|
312
|
+
# and the order of the elements in the returned Array corresponds to the input.
|
313
|
+
#
|
314
|
+
# @param keys [Array, Integer, Range, String, Symbol] Key for @rules
|
315
|
+
# @return [Proc, Regexp]
|
316
|
+
def rules_at(keys, *rest)
|
317
|
+
if defined?(keys.exclude_end?)
|
318
|
+
return @rules[keys]
|
319
|
+
end
|
320
|
+
([keys]+rest).flatten.map{ |i| rule_at(i) }
|
321
|
+
end
|
322
|
+
private :rules_at
|
323
|
+
|
324
|
+
|
325
|
+
# Pop a rule(s)
|
326
|
+
#
|
327
|
+
# @option *rest [Integer]
|
328
|
+
# @return [Proc, Array<Proc>] if no argument is given, Proc is returned.
|
329
|
+
def pop(*rest)
|
330
|
+
if (rest.size == 0)
|
331
|
+
(@rules.size > 0) ? @names.slice!((@rules.size-1)..-1) : @names.clear
|
332
|
+
else
|
333
|
+
i_beg = @rules.size - rest[0]
|
334
|
+
i_beg = 0 if i_beg < 0
|
335
|
+
@names.slice!(i_beg..-1)
|
336
|
+
end
|
337
|
+
(rest.size == 0) ? @rules.pop : @rules.pop(*rest)
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
# Apply the rules to a given String
|
342
|
+
#
|
343
|
+
# In default, all the rules are applied in the registered sequence, unless an Option is specified
|
344
|
+
#
|
345
|
+
# This method receives either String (for the first-time application only)
|
346
|
+
# or Array (for any subsequent applications), the latter of which is
|
347
|
+
# (though not necessarily) the result of the previous applications,
|
348
|
+
# applies the {#rules} one by one sequentially, and returns an Array.
|
349
|
+
#
|
350
|
+
# Elements of {#rules} can be Regexp (particularly common for for {#rules}[0]).
|
351
|
+
# In that case, if the given argument is a String, String#split is simply applied.
|
352
|
+
# If it is an Array, String#split is applied to every element of an even index
|
353
|
+
# (starting from 0; n.b., all even-index elements are semantically Paragraphs).
|
354
|
+
# Importantly, this manipulation with String#split to Array unfolds the result
|
355
|
+
# of split on the spot, which means in general it increases
|
356
|
+
# the number of the elements (Array#size) from the given one. For example,
|
357
|
+
# suppose the given Array has initially two elements and then String#split
|
358
|
+
# is applied to the first element only (because it is the only even-index element).
|
359
|
+
# Suppose the application creates 3 elements. They are interpreted as
|
360
|
+
# a sequence of Paragraph, Boundary, and Paragraph. Then the returned array
|
361
|
+
# will contain 4 elements. Or, suppose the split application to the first element
|
362
|
+
# of the given array resulted in an array of 4 elements. Then, the last element
|
363
|
+
# of this array and the next element of the original array are both Boundary.
|
364
|
+
# In this case, the two Boundaries are merged so that the elements of
|
365
|
+
# the returned array are in the right order of Paragraphs and Boundaries.
|
366
|
+
#
|
367
|
+
# @example String input
|
368
|
+
# pr = PlainText::Part::ParseRule /(\n)/
|
369
|
+
# pr.rules #=> [/(\n)/]
|
370
|
+
# pr.apply(["abc==def==\n"])
|
371
|
+
# #=> ["abc==def==", "\n"])
|
372
|
+
#
|
373
|
+
# @example Array input
|
374
|
+
# pr.rules #=> [/(==)/]
|
375
|
+
# pr.apply(["abc==def==", "\n"])
|
376
|
+
# #=> ["abc", "==", "def", "==\n"])
|
377
|
+
#
|
378
|
+
# @example String input, sequential processing
|
379
|
+
# pr.rules #=> [/(\n)/, /(==)/]
|
380
|
+
# pr.apply(["abc==def==\n"])
|
381
|
+
# #=> ["abc", "==", "def", "==\n"])
|
382
|
+
#
|
383
|
+
# @example Regexp and Proc rules, applied one by one.
|
384
|
+
# pr = PlainText::Part::ParseRule /(==(?:\n)?)/, index: 'my_first'
|
385
|
+
# pr.push{ |i| i.map{|j| ("def"==j) ? PlainText::Part::Paragraph(j) : j}}
|
386
|
+
# pr.rules
|
387
|
+
# #=> [/(==(?:\n)?)/, Proc{ |i| i.map{|j| ("def"==j) ? i.upcase : j}}]
|
388
|
+
# ar0 = pr.apply(["abc==def==\n"], index: 'my_first')
|
389
|
+
# #=> ["abc", "==", "def", "==\n"])
|
390
|
+
# pr.apply ar0, index: 1
|
391
|
+
# #=> ["abc", "==", "DEF", "==\n"])
|
392
|
+
#
|
393
|
+
# @param inprm [String, Array, PlainText::Part]
|
394
|
+
# @param index: [Array, Range, Integer, String, Symbol] If given, the rule(s) at the given index (indices) or key(s) only are applied in the given order.
|
395
|
+
# @return [Array] array of String, Paragraph, Boundary, Array, Part, etc
|
396
|
+
def apply(inprm, index: nil, from_string: true, from_array: true)
|
397
|
+
allrules = (index ? rules_at(index) : @rules)
|
398
|
+
|
399
|
+
arret = (inprm.class.method_defined?(:to_ary) ? inprm : [inprm])
|
400
|
+
allrules.each do |each_r|
|
401
|
+
arret = (defined?(each_r.match) ? apply_split(arret, each_r) : each_r.call(arret))
|
402
|
+
end
|
403
|
+
arret
|
404
|
+
end
|
405
|
+
|
406
|
+
# Apply String#split with Regexp
|
407
|
+
#
|
408
|
+
# If an Array is given, Regexp is applied to each of even-number elements,
|
409
|
+
# which are supposed to be {Paragraph}, one by one and recursively.
|
410
|
+
#
|
411
|
+
# @param inprm [String, Array, PlainText::Part]
|
412
|
+
# @param re [Regexp]
|
413
|
+
# @return [Array]
|
414
|
+
def apply_split(inprm, re)
|
415
|
+
return inprm.split re if !defined? inprm.to_ary
|
416
|
+
|
417
|
+
hsflag = { concat_bd: false } # whether concatnate Boundary to the previous one as a String.
|
418
|
+
arret = []
|
419
|
+
inprm.each_with_index do |ea_e, i|
|
420
|
+
if i.odd?
|
421
|
+
if !hsflag[:concat_bd]
|
422
|
+
arret << ea_e
|
423
|
+
next
|
424
|
+
end
|
425
|
+
if defined? ea_e.to_ary
|
426
|
+
# The given argument (by the user) is wrong! Boundary is somehow an Array.
|
427
|
+
# Here, an empty string is added, emulating an empty Paragraph.
|
428
|
+
arret << "" << ea_e
|
429
|
+
else
|
430
|
+
# Boundary is concatnated with the previous one.
|
431
|
+
arret[-1] << ea_e
|
432
|
+
end
|
433
|
+
hsflag[:concat_bd] = false
|
434
|
+
next
|
435
|
+
end
|
436
|
+
|
437
|
+
ar = apply_split(ea_e, re)
|
438
|
+
|
439
|
+
if (defined? ea_e.to_ary)
|
440
|
+
# The processed Array(Part) simply replaces the existing one (no change of the size of the given array).
|
441
|
+
arret << ar
|
442
|
+
else
|
443
|
+
# String(Paragraph) is split further and concatnated on the spot.
|
444
|
+
ar = [""] if ar.empty?
|
445
|
+
arret.concat ar
|
446
|
+
hsflag[:concat_bd] = true if ar.size.even? # The next element (Boundary) should be appended to the last element as String.
|
447
|
+
end
|
448
|
+
end
|
449
|
+
arret
|
450
|
+
end
|
451
|
+
private :apply_split
|
452
|
+
|
453
|
+
# @return [Integer] The number of defined rules.
|
454
|
+
def size
|
455
|
+
si_rules = rules.size
|
456
|
+
si_names = names.size
|
457
|
+
if si_rules != si_names
|
458
|
+
warn "WARNING: Inconsistent sizes for between rules (#{si_rules}) and names (#{si_names})."
|
459
|
+
end
|
460
|
+
si_rules
|
461
|
+
end
|
462
|
+
|
463
|
+
def_lb_q = PlainText::DefLineBreaks.map{|i| Regexp.quote i}.join '|'
|
464
|
+
|
465
|
+
# {ParseRule} instance to
|
466
|
+
# split a String with 2 or more linebreaks (with potentially white-spaces in between).
|
467
|
+
# This instance can be dup-ped and used normally. However, if it is clone-d, the cloned instance would be unmodifiable.
|
468
|
+
RuleConsecutiveLbs = self.new(/((?:#{def_lb_q})(?:#{def_lb_q}|[[:blank:]])*(?:#{def_lb_q}))/, name: 'ConsecutiveLbs') # => /((?:\r\n|\n|\r){2,}/
|
469
|
+
RuleConsecutiveLbs.freeze
|
470
|
+
RuleConsecutiveLbs.rules.freeze
|
471
|
+
RuleConsecutiveLbs.names.freeze
|
472
|
+
end # class ParseRule
|
473
|
+
end # module PlainText
|
474
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module PlainText
|
4
|
+
class Part < Array
|
5
|
+
|
6
|
+
# Class to express a Boundary as String
|
7
|
+
#
|
8
|
+
class Boundary < String
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
def inspect
|
12
|
+
# 'Boundary("\n\n\n")'
|
13
|
+
s = self.class.name
|
14
|
+
sprintf "%s(%s)", (s.split('::')[2..-1].join('::') rescue s), super
|
15
|
+
end
|
16
|
+
|
17
|
+
# Boundary sub-class name only
|
18
|
+
#
|
19
|
+
# Make sure your class is a child class of Boundary.
|
20
|
+
# Otherwise this method would not be inherited, obviously.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# class PlainText::Part::Boundary
|
24
|
+
# class SubBoundary < self
|
25
|
+
# class SubBoundary < self; end # It must be a child class!
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# ss = PlainText::Part::SubBoundary::SubSubBoundary.new ["abc"]
|
29
|
+
# ss.subclass_name # => "SubBoundary::SubSubBoundary"
|
30
|
+
#
|
31
|
+
# @return [String]
|
32
|
+
# @see PlainText::Part#subclass_name
|
33
|
+
def subclass_name
|
34
|
+
printf "__method__=(%s)\n", __method__
|
35
|
+
self.class.name.split(/\A#{Regexp.quote method(__method__).owner.name}::/)[1] || ''
|
36
|
+
end
|
37
|
+
|
38
|
+
# Empty Boundary instance
|
39
|
+
Empty = self.new ""
|
40
|
+
Empty.freeze
|
41
|
+
end # class Boundary < String
|
42
|
+
end # class Part < Array
|
43
|
+
end # module PlainText
|
44
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module PlainText
|
4
|
+
class Part < Array
|
5
|
+
|
6
|
+
# Class to express a Paragraph as String
|
7
|
+
#
|
8
|
+
class Paragraph < String
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
def inspect
|
12
|
+
# 'Paragraph("abc\ndef")' or like 'Paragraph::Title("My Title")'
|
13
|
+
s = self.class.name
|
14
|
+
sprintf "%s(%s)", (s.split('::')[2..-1].join('::') rescue s), super
|
15
|
+
end
|
16
|
+
|
17
|
+
# Paragraph sub-class name
|
18
|
+
#
|
19
|
+
# Make sure your class is a child class of Paragraph.
|
20
|
+
# Otherwise this method would not be inherited, obviously.
|
21
|
+
#
|
22
|
+
# @return [String]
|
23
|
+
# @see PlainText::Part#subclass_name
|
24
|
+
def subclass_name
|
25
|
+
printf "__method__=(%s)\n", __method__
|
26
|
+
self.class.name.split(/\A#{Regexp.quote method(__method__).owner.name}::/)[1] || ''
|
27
|
+
end
|
28
|
+
|
29
|
+
# Empty Paragraph instance
|
30
|
+
Empty = self.new ""
|
31
|
+
Empty.freeze
|
32
|
+
end # class Paragraph < String
|
33
|
+
end # class Part < Array
|
34
|
+
end # module PlainText
|
35
|
+
|