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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.document +11 -0
  3. data/.gitignore +17 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +3 -0
  6. data/.yardopts +3 -0
  7. data/Gemfile +17 -0
  8. data/LICENSE +20 -0
  9. data/README.md +345 -0
  10. data/Rakefile +21 -0
  11. data/bin/bowler +44 -0
  12. data/config/bowlers/cmd.yaml +3 -0
  13. data/config/bowlers/html.yaml +128 -0
  14. data/config/bowlers/html_supplemental.yaml +3 -0
  15. data/config/bowlers/markdown2html.yaml +23 -0
  16. data/extras/demo/main.rb +34 -0
  17. data/extras/demo/public/apple-touch-icon-114x114-precomposed.png +0 -0
  18. data/extras/demo/public/apple-touch-icon-144x144-precomposed.png +0 -0
  19. data/extras/demo/public/apple-touch-icon-57x57-precomposed.png +0 -0
  20. data/extras/demo/public/apple-touch-icon-72x72-precomposed.png +0 -0
  21. data/extras/demo/public/apple-touch-icon-precomposed.png +0 -0
  22. data/extras/demo/public/apple-touch-icon.png +0 -0
  23. data/extras/demo/public/css/bootstrap-theme.css +384 -0
  24. data/extras/demo/public/css/bootstrap-theme.min.css +1 -0
  25. data/extras/demo/public/css/bootstrap.css +6805 -0
  26. data/extras/demo/public/css/bootstrap.min.css +9 -0
  27. data/extras/demo/public/css/main.css +22 -0
  28. data/extras/demo/public/favicon.ico +0 -0
  29. data/extras/demo/public/fonts/glyphicons-halflings-regular.eot +0 -0
  30. data/extras/demo/public/fonts/glyphicons-halflings-regular.svg +228 -0
  31. data/extras/demo/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  32. data/extras/demo/public/fonts/glyphicons-halflings-regular.woff +0 -0
  33. data/extras/demo/public/html.html +262 -0
  34. data/extras/demo/public/index.html +110 -0
  35. data/extras/demo/public/js/main.js +1 -0
  36. data/extras/demo/public/js/vendor/bootstrap.js +1999 -0
  37. data/extras/demo/public/js/vendor/bootstrap.min.js +6 -0
  38. data/extras/demo/public/js/vendor/jquery-1.10.1.min.js +6 -0
  39. data/extras/demo/public/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js +11 -0
  40. data/extras/drafts/parsing.md +137 -0
  41. data/extras/support/typo +66 -0
  42. data/features/bowler.feature +8 -0
  43. data/features/html.feature +229 -0
  44. data/features/step_definitions/bowler_steps.rb +39 -0
  45. data/features/step_definitions/html_steps.rb +11 -0
  46. data/features/support/env.rb +7 -0
  47. data/images/owl-old.png +0 -0
  48. data/images/owl-old.xcf +0 -0
  49. data/images/owl.png +0 -0
  50. data/images/owl.xcf +0 -0
  51. data/lib/qipowl/bowlers/cmd.rb +26 -0
  52. data/lib/qipowl/bowlers/html.rb +409 -0
  53. data/lib/qipowl/bowlers/htmldoc.rb +268 -0
  54. data/lib/qipowl/bowlers/yaml.rb +63 -0
  55. data/lib/qipowl/core/bowler.rb +251 -0
  56. data/lib/qipowl/core/mapper.rb +92 -0
  57. data/lib/qipowl/core/monkeypatches.rb +168 -0
  58. data/lib/qipowl/core/ruler.rb +106 -0
  59. data/lib/qipowl/utils/hash_recursive_merge.rb +72 -0
  60. data/lib/qipowl/utils/logging.rb +14 -0
  61. data/lib/qipowl/version.rb +3 -0
  62. data/lib/qipowl.rb +50 -0
  63. data/qipowl.gemspec +42 -0
  64. data/qipowl.komodoproject +4 -0
  65. data/spec/bowler_spec.rb +11 -0
  66. data/spec/spec_helper.rb +15 -0
  67. data/spec/string_spec.rb +32 -0
  68. data/spec/yaml_test.yaml +10 -0
  69. 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