qipowl 0.9.0
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/.document +11 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/.yardopts +3 -0
- data/Gemfile +17 -0
- data/LICENSE +20 -0
- data/README.md +345 -0
- data/Rakefile +21 -0
- data/bin/bowler +44 -0
- data/config/bowlers/cmd.yaml +3 -0
- data/config/bowlers/html.yaml +128 -0
- data/config/bowlers/html_supplemental.yaml +3 -0
- data/config/bowlers/markdown2html.yaml +23 -0
- data/extras/demo/main.rb +34 -0
- data/extras/demo/public/apple-touch-icon-114x114-precomposed.png +0 -0
- data/extras/demo/public/apple-touch-icon-144x144-precomposed.png +0 -0
- data/extras/demo/public/apple-touch-icon-57x57-precomposed.png +0 -0
- data/extras/demo/public/apple-touch-icon-72x72-precomposed.png +0 -0
- data/extras/demo/public/apple-touch-icon-precomposed.png +0 -0
- data/extras/demo/public/apple-touch-icon.png +0 -0
- data/extras/demo/public/css/bootstrap-theme.css +384 -0
- data/extras/demo/public/css/bootstrap-theme.min.css +1 -0
- data/extras/demo/public/css/bootstrap.css +6805 -0
- data/extras/demo/public/css/bootstrap.min.css +9 -0
- data/extras/demo/public/css/main.css +22 -0
- data/extras/demo/public/favicon.ico +0 -0
- data/extras/demo/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/extras/demo/public/fonts/glyphicons-halflings-regular.svg +228 -0
- data/extras/demo/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/extras/demo/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/extras/demo/public/html.html +262 -0
- data/extras/demo/public/index.html +110 -0
- data/extras/demo/public/js/main.js +1 -0
- data/extras/demo/public/js/vendor/bootstrap.js +1999 -0
- data/extras/demo/public/js/vendor/bootstrap.min.js +6 -0
- data/extras/demo/public/js/vendor/jquery-1.10.1.min.js +6 -0
- data/extras/demo/public/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js +11 -0
- data/extras/drafts/parsing.md +137 -0
- data/extras/support/typo +66 -0
- data/features/bowler.feature +8 -0
- data/features/html.feature +229 -0
- data/features/step_definitions/bowler_steps.rb +39 -0
- data/features/step_definitions/html_steps.rb +11 -0
- data/features/support/env.rb +7 -0
- data/images/owl-old.png +0 -0
- data/images/owl-old.xcf +0 -0
- data/images/owl.png +0 -0
- data/images/owl.xcf +0 -0
- data/lib/qipowl/bowlers/cmd.rb +26 -0
- data/lib/qipowl/bowlers/html.rb +409 -0
- data/lib/qipowl/bowlers/htmldoc.rb +268 -0
- data/lib/qipowl/bowlers/yaml.rb +63 -0
- data/lib/qipowl/core/bowler.rb +251 -0
- data/lib/qipowl/core/mapper.rb +92 -0
- data/lib/qipowl/core/monkeypatches.rb +168 -0
- data/lib/qipowl/core/ruler.rb +106 -0
- data/lib/qipowl/utils/hash_recursive_merge.rb +72 -0
- data/lib/qipowl/utils/logging.rb +14 -0
- data/lib/qipowl/version.rb +3 -0
- data/lib/qipowl.rb +50 -0
- data/qipowl.gemspec +42 -0
- data/qipowl.komodoproject +4 -0
- data/spec/bowler_spec.rb +11 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/string_spec.rb +32 -0
- data/spec/yaml_test.yaml +10 -0
- metadata +254 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative '../core/bowler'
|
4
|
+
|
5
|
+
module Qipowl
|
6
|
+
|
7
|
+
# Markup processor for Yaml.
|
8
|
+
#
|
9
|
+
# This class produces hash from YAML file.
|
10
|
+
class Yaml < Bowler
|
11
|
+
attr_reader :result
|
12
|
+
def initialize file = nil
|
13
|
+
super
|
14
|
+
merge_rules file if file
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_and_roll str
|
18
|
+
@result = {}
|
19
|
+
@partial = nil
|
20
|
+
super str
|
21
|
+
@result
|
22
|
+
end
|
23
|
+
|
24
|
+
# Tupla handler
|
25
|
+
def : *args
|
26
|
+
from, till, *rest = args.flatten
|
27
|
+
if @partial.nil? or Hash === @partial
|
28
|
+
(@partial ||= {})[from.unuglify] = till.unuglify
|
29
|
+
rest
|
30
|
+
else
|
31
|
+
harvest ::, args.join(SEPARATOR).unbowl.unspacefy.uncarriage.strip
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Array element handler
|
36
|
+
def - *args
|
37
|
+
(@partial ||= []) << args.join(SEPARATOR).unuglify
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def harvest callee, str
|
42
|
+
if Hash === @partial
|
43
|
+
if str == String::CARRIAGE_RETURN
|
44
|
+
key = @partial.keys.last
|
45
|
+
@partial.delete key
|
46
|
+
@result[key] = @partial
|
47
|
+
else
|
48
|
+
@result.merge! @partial
|
49
|
+
end
|
50
|
+
else
|
51
|
+
@result[str] = @partial unless str.vacant?
|
52
|
+
end
|
53
|
+
|
54
|
+
@partial = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.parse str
|
58
|
+
Yaml.new.parse_and_roll str
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'monkeypatches'
|
4
|
+
require_relative '../utils/logging'
|
5
|
+
|
6
|
+
# @author Alexei Matyushkin
|
7
|
+
module Qipowl::Bowlers
|
8
|
+
|
9
|
+
# Base class for all the parsers.
|
10
|
+
#
|
11
|
+
# Technically it may be instantiated, but that’s meaningless.
|
12
|
+
# Main operation method for it and all the descendants is
|
13
|
+
# {#parse}. It sequentially executes following
|
14
|
+
# private methods:
|
15
|
+
#
|
16
|
+
# - {#defreeze}
|
17
|
+
# - {#roast}
|
18
|
+
# - {#serveup}
|
19
|
+
#
|
20
|
+
# Normally the developer does not need to interfere the {#roast}
|
21
|
+
# method which proceeds the input string. To prepare the input
|
22
|
+
# for +evaluation+ one overwrites {#defreeze}, for some afterwork
|
23
|
+
# the {#serveup} method is here.
|
24
|
+
#
|
25
|
+
# Descendants are supposed to overwrite {#method_missing} for some
|
26
|
+
# custom processing and introduce DSL methods, which will be executed
|
27
|
+
# by `eval` inside the {#roast} method.
|
28
|
+
#
|
29
|
+
# Instance variables `:in` and `:out` are here to gain an access to
|
30
|
+
# the last interpreted input string and the result of evaluation
|
31
|
+
# respectively.
|
32
|
+
#
|
33
|
+
class Bowler
|
34
|
+
include TypoLogging
|
35
|
+
|
36
|
+
attr_reader :in, :out
|
37
|
+
|
38
|
+
# Internal constant for joining/splitting the strings during processing.
|
39
|
+
# Override on your own risk. I can′t imagine why you would need to do so.
|
40
|
+
SEPARATOR = $, || ' '
|
41
|
+
|
42
|
+
def execute str
|
43
|
+
@out = (serveup roast defreeze @in = str)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Everything is a DSL, remember?
|
47
|
+
#
|
48
|
+
# @return true
|
49
|
+
def respond_to?(method)
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Everything is a DSL, remember? Even constants.
|
54
|
+
# @todo This fails to do with DSLing words, beginning with capitals :-(
|
55
|
+
#
|
56
|
+
# @return the constant name as is
|
57
|
+
def self.const_missing name
|
58
|
+
raise "There was CONST [#{name}] met. Stupid programming error."
|
59
|
+
name
|
60
|
+
end
|
61
|
+
|
62
|
+
# If somebody needs to interfere the standard processing,
|
63
|
+
# she supposed to introduce `special_handler` method. The descendants
|
64
|
+
# will be processed before standard operation (which in fact simply
|
65
|
+
# collects words within an array one by one.)
|
66
|
+
def method_missing method, *args, &block
|
67
|
+
method, *args = special_handler(method, *args, &block) \
|
68
|
+
if self.private_methods.include?(:special_handler)
|
69
|
+
[method, args].flatten
|
70
|
+
end
|
71
|
+
|
72
|
+
# Adds new +entity+ in the section specified.
|
73
|
+
# E. g., call to
|
74
|
+
#
|
75
|
+
# add_spice :linewide, :°, :deg, :degrees
|
76
|
+
#
|
77
|
+
# in HTML implementation adds a support for specifying something like:
|
78
|
+
#
|
79
|
+
# ° 15
|
80
|
+
# ° 30
|
81
|
+
# ° 45
|
82
|
+
#
|
83
|
+
# which is to be converted to the following:
|
84
|
+
#
|
85
|
+
# <degrees>
|
86
|
+
# <deg>15</deg>
|
87
|
+
# <deg>30</deg>
|
88
|
+
# <deg>45</deg>
|
89
|
+
# </degrees>
|
90
|
+
#
|
91
|
+
# @param [Symbol] section the section (it must be one of {Mapping.SPICES}) to add new key to
|
92
|
+
# @param [Symbol] key the name for the key
|
93
|
+
# @param [Symbol] value the value
|
94
|
+
# @param [Symbol] enclosure_value optional value to be added for the key into enclosures section
|
95
|
+
def add_entity section, key, value, enclosure_value = null
|
96
|
+
if (tags = self.class.const_get("#{section.upcase}_TAGS"))
|
97
|
+
key = key.bowl.to_sym
|
98
|
+
tags[key] = value.to_sym
|
99
|
+
self.class.const_get("ENCLOSURES_TAGS")[key] = enclosure_value.to_sym if enclosure_value
|
100
|
+
self.class.class_eval %Q{
|
101
|
+
alias_method :#{key}, :∀_#{section}
|
102
|
+
} # unless self.class.instance_methods(true).include?(key.bowl)
|
103
|
+
else
|
104
|
+
logger.warn "Trying to add key “#{key}” in an invalid section “#{section}”. Ignoring…"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Removes key from both {Mapping.SPICES} and {Mapping.SALT}. See {#add_spice}
|
109
|
+
#
|
110
|
+
# @param [Symbol] key the key to be removed
|
111
|
+
def remove_entity key
|
112
|
+
%w(block alone magnet grip regular).each { |section|
|
113
|
+
next unless send :"∃_#{section}_tag", key.to_sym
|
114
|
+
|
115
|
+
self.class.const_get("#{section.upcase}_TAGS").delete key
|
116
|
+
self.class.const_get("ENCLOSURES_TAGS").delete key
|
117
|
+
self.class.class_eval %Q{
|
118
|
+
remove_method :#{key.bowl}
|
119
|
+
}
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
protected
|
125
|
+
%w(block alone magnet grip regular).each { |section|
|
126
|
+
define_method "∀_#{section}".to_sym, ->(*args) {
|
127
|
+
raise "Default method for #{section} (“#{self.class.name.gsub(/_\d+\Z/, '')}#∀_#{section}”) MUST be defined"
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
def defreeze str
|
132
|
+
str
|
133
|
+
end
|
134
|
+
|
135
|
+
def roast str
|
136
|
+
(split str).reverse.map { |dish|
|
137
|
+
@yielded = []
|
138
|
+
rest = begin
|
139
|
+
eval(dish.strip.carriage)
|
140
|
+
rescue Exception => e
|
141
|
+
msg = e.message.dup
|
142
|
+
logger.error '='*78
|
143
|
+
logger.error "Could not roast dish [#{msg.force_encoding(Encoding::UTF_8)}].\nWill return as is… Dish follows:"
|
144
|
+
logger.error '-'*78
|
145
|
+
logger.error dish
|
146
|
+
logger.error '='*78
|
147
|
+
[*dish]
|
148
|
+
end
|
149
|
+
harvest(nil, orphan([*rest].join(SEPARATOR))) # FIXME Check if this is correct in all the cases
|
150
|
+
@yielded.pop(@yielded.size).reverse.join(SEPARATOR)
|
151
|
+
}.reverse.join($/).uncarriage.un␚ify.unspacefy.unbowl
|
152
|
+
end
|
153
|
+
|
154
|
+
def serveup str
|
155
|
+
str.gsub(/⌦./, '').gsub(/.⌫/, '')
|
156
|
+
end
|
157
|
+
|
158
|
+
protected
|
159
|
+
# The handler of the last “orphaned” text block in the input string.
|
160
|
+
#
|
161
|
+
# E.g.:
|
162
|
+
#
|
163
|
+
# Here goes a quite significant list:
|
164
|
+
#
|
165
|
+
# • line item 1
|
166
|
+
# • nested li1
|
167
|
+
# • nested li 2
|
168
|
+
# • line item 2
|
169
|
+
#
|
170
|
+
# While all line items are operated by `•` method, the top sentence
|
171
|
+
# is orphaned (has no prepnding DSL method to be called on.)
|
172
|
+
# Since we still need to handle it somehow, the {#orphan} method is here.
|
173
|
+
#
|
174
|
+
# @param [String] str the string to be operated “bu default rule”
|
175
|
+
# @return [String] the processed input (in derivatives, here it returns the untouched input string itself)
|
176
|
+
def orphan str
|
177
|
+
str
|
178
|
+
end
|
179
|
+
|
180
|
+
# The handler for harvesting partial result.
|
181
|
+
#
|
182
|
+
# Processing sometimes calls this method, designating the meaningful
|
183
|
+
# part of input text is done and should be yielded. E.g. when the
|
184
|
+
# block of code is processed:
|
185
|
+
#
|
186
|
+
# Λ ruby
|
187
|
+
# @mapping[:inplace].each { |tag, htmltag|
|
188
|
+
# do_smth tag, htmltag
|
189
|
+
# }
|
190
|
+
# Λ
|
191
|
+
#
|
192
|
+
# After we have this part of input processed, it should be considered
|
193
|
+
# “done.” So block processors call {#harvest} to store processed parts.
|
194
|
+
#
|
195
|
+
# @param [Symbol] callee of this method. Qipowl hardly relies on method namings and sometimes we may need to know if the call was made by, say, lineitem DSL (`•`), not datalist (`▷`).
|
196
|
+
# @param [String] str string to yield
|
197
|
+
#
|
198
|
+
# @return nil
|
199
|
+
def harvest callee, str
|
200
|
+
@yielded << str unless str.vacant?
|
201
|
+
nil
|
202
|
+
end
|
203
|
+
|
204
|
+
private
|
205
|
+
# Prepares blocks in the input for the execution
|
206
|
+
def block str
|
207
|
+
result = str.dup
|
208
|
+
self.class::BLOCK_TAGS.each { |tag, value|
|
209
|
+
result.gsub!(/(#{tag})\s*(\S*\s*|$)(.*?)(#{tag}|\Z)/m) {
|
210
|
+
%Q{
|
211
|
+
|
212
|
+
#{$1}('#{$2.strip}', '#{$3.carriage}')
|
213
|
+
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
result
|
218
|
+
end
|
219
|
+
|
220
|
+
# Prepares customs in the input for the execution
|
221
|
+
def custom str
|
222
|
+
result = str.unbowl.dup
|
223
|
+
self.class::CUSTOM_TAGS.each { |tag, value|
|
224
|
+
result.gsub!(/#{tag}/, value)
|
225
|
+
}
|
226
|
+
result.bowl
|
227
|
+
end
|
228
|
+
|
229
|
+
# Prepares grips in the input for the execution
|
230
|
+
# FIX<E There is a problem: we append a trailing space, need to remove it later!!
|
231
|
+
def grip str
|
232
|
+
result = str.dup
|
233
|
+
self.class::GRIP_TAGS.each { |tag, value|
|
234
|
+
result.gsub!(/(?:#{tag})(.*?)(?:#{tag})/m) {
|
235
|
+
next if (args = $1).vacant?
|
236
|
+
tag = value[:marker] if Hash === value && value[:marker]
|
237
|
+
"⌦ #{tag} #{args}#{tag}∎⌫"
|
238
|
+
}
|
239
|
+
}
|
240
|
+
result
|
241
|
+
end
|
242
|
+
|
243
|
+
def split str
|
244
|
+
(block str.bowl).split(/\R{2,}/).map { |para|
|
245
|
+
para =~ /\A(#{self.class::BLOCK_TAGS.keys.join('|')})\(/ ?
|
246
|
+
para : (grip custom para)
|
247
|
+
}
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require_relative '../core/monkeypatches'
|
6
|
+
require_relative '../utils/hash_recursive_merge'
|
7
|
+
require_relative '../utils/logging'
|
8
|
+
|
9
|
+
# @author Alexei Matyushkin
|
10
|
+
module Qipowl::Mappers
|
11
|
+
|
12
|
+
# Operates +mapping+ for loaded +YAML+ rules files.
|
13
|
+
#
|
14
|
+
# - For top level sections, each section name
|
15
|
+
# should have the corresponding method within Mapping class;
|
16
|
+
# default is `[:includes]` which is processed with {#includes}
|
17
|
+
# method which is simply loads rules from the respective file
|
18
|
+
#
|
19
|
+
# Mapping may be loaded from YAML file, as well as be merged
|
20
|
+
# against other YAML file, hash or `Ruler` instance.
|
21
|
+
class Mapper
|
22
|
+
def initialize input = nil
|
23
|
+
@hash = {}
|
24
|
+
merge!(input) unless input.nil?
|
25
|
+
end
|
26
|
+
def to_hash
|
27
|
+
@hash
|
28
|
+
end
|
29
|
+
def merge! input
|
30
|
+
input = load_yaml(input) if input.is_one_of?(String, IO)
|
31
|
+
raise ArgumentError.new "Invalid map for merge in Mapper" \
|
32
|
+
unless input.respond_to? :to_hash
|
33
|
+
|
34
|
+
incs = input.delete(:includes)
|
35
|
+
|
36
|
+
@entities_dirty = true
|
37
|
+
@hash.rmerge!(input.to_hash)
|
38
|
+
incs.each { |inc|
|
39
|
+
merge! inc
|
40
|
+
} rescue NoMethodError
|
41
|
+
end
|
42
|
+
private
|
43
|
+
# FIXME Make file search more flexible!!!
|
44
|
+
def load_yaml input
|
45
|
+
IO === input ?
|
46
|
+
YAML.load_stream(input) :
|
47
|
+
YAML.load_file("#{Qipowl.bowlers_dir}/#{input.downcase}.yaml")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class BowlerMapper < Mapper
|
52
|
+
def initialize input = nil
|
53
|
+
input = self.class.name.split('::').last.downcase.gsub(/bowlermapper\Z/, '') if input.nil?
|
54
|
+
super input
|
55
|
+
end
|
56
|
+
@entities = nil
|
57
|
+
@entities_dirty = true
|
58
|
+
|
59
|
+
def entities
|
60
|
+
return @entities unless @entities_dirty
|
61
|
+
@entities = {}
|
62
|
+
@hash[:entities].each { |key, value| # :block. :alone etc
|
63
|
+
@entities[key] ||= {}
|
64
|
+
value.each { |k, v|
|
65
|
+
# Append keys
|
66
|
+
@entities[key][k.bowl] = v.dup
|
67
|
+
if Hash === v
|
68
|
+
@entities[key][k.bowl].delete(:synonyms)
|
69
|
+
# Append explicit synonyms
|
70
|
+
v[:synonyms].each { |syn|
|
71
|
+
(@entities[key][syn.bowl] = v.dup).delete(:synonyms)
|
72
|
+
@entities[key][syn.bowl][:marker] = k.bowl
|
73
|
+
} if v[:synonyms]
|
74
|
+
end
|
75
|
+
}
|
76
|
+
}
|
77
|
+
@entities_dirty = false
|
78
|
+
@entities
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class HtmlBowlerMapper < BowlerMapper
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if __FILE__ == $0
|
88
|
+
require '../../qipowl'
|
89
|
+
y = Qipowl::Mappers::BowlerMapper.new 'html'
|
90
|
+
require 'awesome_print'
|
91
|
+
ap y[:mail]
|
92
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative '../utils/hash_recursive_merge'
|
4
|
+
|
5
|
+
module Qipowl
|
6
|
+
class ::Object
|
7
|
+
def is_one_of? *classes
|
8
|
+
!classes.each { |c| break false if c === self }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
class ::Array
|
12
|
+
# Checks whether an array contains non-nil elements
|
13
|
+
# @return +true+ if an array does not contain non-nils and +false+ otherwise
|
14
|
+
def vacant?
|
15
|
+
(self.flatten - [nil]).empty?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Bowling string means producing interpreter-safe text basing on ascii input.
|
20
|
+
#
|
21
|
+
class ::String
|
22
|
+
NBSP = "\u{00A0}"
|
23
|
+
|
24
|
+
SYMBOL_FOR_SPACE = "\u{2420}" # ␠
|
25
|
+
|
26
|
+
WIDESPACE = "\u{FF00}"
|
27
|
+
EN_SPACE = "\u{2002}"
|
28
|
+
EM_SPACE = "\u{2003}"
|
29
|
+
THREE_PER_EM_SPACE = "\u{2004}"
|
30
|
+
FOUR_PER_EM_SPACE = "\u{2005}"
|
31
|
+
SIX_PER_EM_SPACE = "\u{2006}"
|
32
|
+
FIGURE_SPACE = "\u{2007}"
|
33
|
+
PUNCTUATION_SPACE = "\u{2008}"
|
34
|
+
THIN_SPACE = "\u{2009}"
|
35
|
+
HAIR_SPACE = "\u{200A}"
|
36
|
+
ZERO_WIDTH_SPACE = "\u{200B}"
|
37
|
+
NARROW_NO_BREAK_SPACE = "\u{202F}"
|
38
|
+
MEDIUM_MATHEMATICAL_SPACE = "\u{205F}"
|
39
|
+
ZERO_WIDTH_NO_BREAK_SPACE = "\u{FEFF}"
|
40
|
+
IDEOGRAPHIC_SPACE = "\u{3000}"
|
41
|
+
|
42
|
+
CARRIAGE_RETURN = '␍'
|
43
|
+
NULL = '␀'
|
44
|
+
ASCII_SYMBOLS, ASCII_DIGITS, ASCII_LETTERS_SMALL, ASCII_LETTERS_CAP = [
|
45
|
+
[(0x21..0x2F), (0x3A..0x40), (0x5B..0x60), (0x7B..0x7E)],
|
46
|
+
[(0x30..0x39)],
|
47
|
+
[(0x61..0x7A)],
|
48
|
+
[(0x41..0x5A)]
|
49
|
+
].map { |current| current.map(&:to_a).flatten.map { |i| [i].pack('U') } }
|
50
|
+
ASCII_ALL = [ASCII_SYMBOLS, ASCII_DIGITS, ASCII_LETTERS_SMALL, ASCII_LETTERS_CAP]
|
51
|
+
|
52
|
+
CODEPOINT_ORIGIN = 0xFF00 - 0x0020 # For FULLWIDTH characters
|
53
|
+
|
54
|
+
UTF_SYMBOLS, UTF_DIGITS, UTF_LETTERS_SMALL, UTF_LETTERS_CAP = ASCII_ALL.map { |current|
|
55
|
+
Hash[* current.join.each_codepoint.map { |char|
|
56
|
+
[[char].pack("U"), [char + CODEPOINT_ORIGIN].pack("U")]
|
57
|
+
}.flatten]
|
58
|
+
}
|
59
|
+
UTF_ALL = [UTF_SYMBOLS.values, UTF_DIGITS.values, UTF_LETTERS_SMALL.values, UTF_LETTERS_CAP.values]
|
60
|
+
|
61
|
+
UTF_ASCII = UTF_SYMBOLS.merge(UTF_DIGITS).merge(UTF_LETTERS_SMALL).merge(UTF_LETTERS_CAP)
|
62
|
+
ASCII_UTF = UTF_ASCII.invert
|
63
|
+
|
64
|
+
def vacant?
|
65
|
+
nil? || empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
def hsub! hash
|
69
|
+
self.gsub!(/#{hash.keys.join('|')}/, hash)
|
70
|
+
end
|
71
|
+
def hsub hash
|
72
|
+
(out = self.dup).hsub! hash
|
73
|
+
out
|
74
|
+
end
|
75
|
+
def bowl!
|
76
|
+
self.gsub!(/[#{Regexp.quote(ASCII_ALL.join)}]/, UTF_ASCII)
|
77
|
+
end
|
78
|
+
def bowl
|
79
|
+
(out = self.dup).bowl!
|
80
|
+
out
|
81
|
+
end
|
82
|
+
def unbowl!
|
83
|
+
self.gsub!(/[#{Regexp.quote(UTF_ALL.join)}]/, ASCII_UTF)
|
84
|
+
end
|
85
|
+
def unbowl
|
86
|
+
(out = self.dup).unbowl!
|
87
|
+
out
|
88
|
+
end
|
89
|
+
def spacefy!
|
90
|
+
self.gsub!(' ', SYMBOL_FOR_SPACE)
|
91
|
+
end
|
92
|
+
def spacefy
|
93
|
+
(out = self.dup).spacefy!
|
94
|
+
out
|
95
|
+
end
|
96
|
+
def unspacefy!
|
97
|
+
self.gsub!(/#{SYMBOL_FOR_SPACE}/, ' ')
|
98
|
+
end
|
99
|
+
def unspacefy
|
100
|
+
(out = self.dup).unspacefy!
|
101
|
+
out
|
102
|
+
end
|
103
|
+
|
104
|
+
def unuglify
|
105
|
+
self.unbowl.unspacefy.uncarriage.strip
|
106
|
+
end
|
107
|
+
|
108
|
+
HTML_ENTITIES = Hash[[['<', 'lt'], ['>', 'gt'], ['&', 'amp']].map { |k, v| [k.bowl, "&#{v};"] }]
|
109
|
+
|
110
|
+
def carriage
|
111
|
+
self.gsub(/\R/, " #{CARRIAGE_RETURN} ")
|
112
|
+
end
|
113
|
+
def carriage!
|
114
|
+
self.gsub!(/\R/, " #{CARRIAGE_RETURN} ")
|
115
|
+
end
|
116
|
+
def uncarriage
|
117
|
+
self.gsub(/ #{CARRIAGE_RETURN} /, %Q(
|
118
|
+
))
|
119
|
+
end
|
120
|
+
def uncarriage!
|
121
|
+
self.gsub!(/ #{CARRIAGE_RETURN} /, %Q(
|
122
|
+
))
|
123
|
+
end
|
124
|
+
|
125
|
+
def un␚ify
|
126
|
+
self.gsub(/␚(.*?)␚/, '')
|
127
|
+
end
|
128
|
+
|
129
|
+
def wstrip
|
130
|
+
self.gsub(/#{NBSP}/, '')
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_filename
|
134
|
+
self.gsub(/[#{Regexp.quote(ASCII_SYMBOLS.join)}]/, UTF_ASCII).gsub(/\s/, "#{NBSP}")[0..50]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class ::Symbol
|
139
|
+
def dup
|
140
|
+
self.to_s.dup.to_sym
|
141
|
+
end
|
142
|
+
def bowl
|
143
|
+
self.to_s.bowl.to_sym
|
144
|
+
end
|
145
|
+
def unbowl
|
146
|
+
self.to_s.unbowl.to_sym
|
147
|
+
end
|
148
|
+
def spacefy
|
149
|
+
self.to_s.spacefy.to_sym
|
150
|
+
end
|
151
|
+
def unspacefy
|
152
|
+
self.to_s.unspacefy.to_sym
|
153
|
+
end
|
154
|
+
def unuglify
|
155
|
+
self.to_s.unuglify.to_sym
|
156
|
+
end
|
157
|
+
def wstrip
|
158
|
+
self.to_s.wstrip.to_sym
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class ::Fixnum
|
163
|
+
def ␚ify
|
164
|
+
"␚#{self}␚"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative '../utils/logging'
|
4
|
+
|
5
|
+
# @author Alexei Matyushkin
|
6
|
+
module Qipowl
|
7
|
+
|
8
|
+
# Operates +mapping+ for loaded +YAML+ rules files.
|
9
|
+
#
|
10
|
+
# - For top level sections, each section name
|
11
|
+
# should have the corresponding method within Mapping class;
|
12
|
+
# default is `[:includes]` which is processed with {#includes}
|
13
|
+
# method which is simply loads rules from the respective file
|
14
|
+
#
|
15
|
+
# Mapping may be loaded from YAML file, as well as be merged
|
16
|
+
# against other YAML file, hash or `Ruler` instance.
|
17
|
+
module Ruler
|
18
|
+
include TypoLogging
|
19
|
+
extend self
|
20
|
+
|
21
|
+
@@bowlers = {} # FIXME REDIS!!!!
|
22
|
+
@@yamls = {}
|
23
|
+
|
24
|
+
|
25
|
+
def get_bowler id: nil, type: nil
|
26
|
+
@@bowlers[id] || new_bowler(type, true)
|
27
|
+
end
|
28
|
+
|
29
|
+
def new_bowler type, persistent = false
|
30
|
+
yaml, clazz = \
|
31
|
+
case type
|
32
|
+
when Class
|
33
|
+
["#{type.to_s.split('::').last.downcase}", type]
|
34
|
+
when String, Symbol
|
35
|
+
["#{type.to_s.downcase}", Qipowl::Bowlers.const_get(type.to_s.capitalize.to_sym)]
|
36
|
+
end
|
37
|
+
|
38
|
+
raise NameError.new("Invalid bowler type: #{type}") \
|
39
|
+
unless clazz.is_a?(Class) && clazz < Qipowl::Bowlers::Bowler
|
40
|
+
|
41
|
+
id = "#{Time.now.to_i}#{rand(1..1_000_000_000)}"
|
42
|
+
name = "#{clazz.name.split('::').last}_#{id}"
|
43
|
+
clazz = Qipowl::Bowlers.const_set(name, Class.new(clazz))
|
44
|
+
|
45
|
+
teach_class clazz, get_yaml(yaml)
|
46
|
+
|
47
|
+
persistent ? [@@bowlers[id] = clazz.new, id] : clazz.new
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def get_yaml yaml
|
52
|
+
return @@yamls[yaml] if @@yamls[yaml]
|
53
|
+
|
54
|
+
clazz = Qipowl::Mappers.const_get("#{yaml.capitalize}BowlerMapper")
|
55
|
+
raise NameError.new("Invalid mapper type: #{clazz}") \
|
56
|
+
unless clazz.is_a?(Class) && clazz < Qipowl::Mappers::BowlerMapper
|
57
|
+
|
58
|
+
@@yamls[yaml] = clazz.new
|
59
|
+
end
|
60
|
+
def teach_class clazz, mapper
|
61
|
+
clazz.const_set("CUSTOM_TAGS", mapper.to_hash[:custom])
|
62
|
+
clazz.const_set("ENCLOSURES_TAGS", mapper.to_hash[:enclosures])
|
63
|
+
clazz.const_set("TAGS", {})
|
64
|
+
clazz.class_eval %Q{
|
65
|
+
def ∃_enclosures entity
|
66
|
+
self.class::ENCLOSURES_TAGS.each { |k, v|
|
67
|
+
next unless k == entity
|
68
|
+
v = {:tag => v} unless Hash === v
|
69
|
+
v[:origin] = self.class::TAGS[v[:tag]]
|
70
|
+
return v
|
71
|
+
}
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
}
|
75
|
+
%w(block alone magnet grip regular).each { |section|
|
76
|
+
clazz.const_set("#{section.upcase}_TAGS", mapper.entities[section.to_sym])
|
77
|
+
clazz.class_eval %Q{
|
78
|
+
#{clazz}::TAGS.rmerge! #{clazz}::#{section.upcase}_TAGS
|
79
|
+
def ∃_#{section} entity
|
80
|
+
self.class::#{section.upcase}_TAGS.each { |k, v|
|
81
|
+
next unless k == entity
|
82
|
+
v = {:tag => v} unless Hash === v
|
83
|
+
v[:section] = k
|
84
|
+
return v
|
85
|
+
}
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
def ∃_#{section}_tag entity
|
89
|
+
∃_#{section}(entity)[:tag] if ∃_#{section}(entity)
|
90
|
+
end
|
91
|
+
}
|
92
|
+
mapper.entities[section.to_sym].each { |key, value|
|
93
|
+
tag = Hash === value && value[:marker] ? value[:marker] : "∀_#{section}"
|
94
|
+
clazz.class_eval %Q{
|
95
|
+
alias_method :#{key}, :#{tag}
|
96
|
+
} unless clazz.instance_methods.include?(key)
|
97
|
+
}
|
98
|
+
}
|
99
|
+
clazz.class_eval %Q{
|
100
|
+
def ∀_tags
|
101
|
+
self.class::TAGS.keys
|
102
|
+
end
|
103
|
+
}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|