infoboxer 0.3.0 → 0.3.1.pre

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: be65bc91a5370bc24553e500754f413196caed76
4
- data.tar.gz: d24dca5a13a64d563ddc473197a732a9f95884fd
3
+ metadata.gz: 0ebc38c153d481aca588625caf2bcb576046afa9
4
+ data.tar.gz: 4f0ccc3b3130403a7a1e35adfc3977d954ac5a66
5
5
  SHA512:
6
- metadata.gz: 661c06d6703db103035f61f55ebee5f0bc8a5f9ad182fcaf2a22be9f91063a9abd80bac1793fabae436b28bed274fcb7908219ba07453f46271a25d1cba0367a
7
- data.tar.gz: 25029633e6516c30a7de21433db1903ab923af2c74082b3a3c9322b50b170cdb104652b95d261b3033a070629ee478a8b5510a7298fbef830d1445dfe56157d0
6
+ metadata.gz: 4593f04e93b2714f13f9abb68cb42cec84ad258d786c815a99ca95d150ba55d597106d1e9e71d3802177cce4a4ad5bf1de0b1f5fa4cc733af19d673fbc600945
7
+ data.tar.gz: 1dc93e4fc257a4cee3fd2c9eb263b24f1a9d8b755a3765bfdf71d1617f7ba9a4ee7d9323502dbee23e48dc85bb6899facb7eadcc62ccc386d958dfca6f28fe5e
data/.rubocop_todo.yml CHANGED
@@ -1,16 +1 @@
1
- # This configuration was generated by
2
- # `rubocop --auto-gen-config`
3
- # on 2017-06-23 13:52:16 +0300 using RuboCop version 0.49.1.
4
- # The point is for the user to remove these configuration records
5
- # one by one as the offenses are removed from the code base.
6
- # Note that changes in the inspected code, or installation of new
7
- # versions of RuboCop, may require this file to be generated again.
8
-
9
- # Offense count: 1
10
- Metrics/AbcSize:
11
- Max: 29
12
-
13
- # Offense count: 1
14
- Metrics/PerceivedComplexity:
15
- Max: 10
16
1
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Infoboxer's change log
2
2
 
3
+ ## 0.3.1.pre (2017-09-16)
4
+
5
+ * Introduce interwiki links following (and proper handling of interwikis, in general);
6
+ * Add `<gallery>` tag support;
7
+ * Introduce `Navigation::Selector#===`;
8
+ * Much more `Enumerable`'s methods supported by `Nodes`;
9
+ * Lot of small simplifications, cleanups and bugfixes.
10
+
11
+ TBH, it should be 0.4.0 or more, but it would be a shame to change versions so fast :) So, at least
12
+ until it is `-pre`, let it be 0.3.1.
13
+
3
14
  ## 0.3.0 (2017-07-23)
4
15
 
5
16
  * Change logic of navigation through templates; now templates contents aren't hidden from global
data/lib/infoboxer.rb CHANGED
@@ -72,8 +72,8 @@ module Infoboxer
72
72
  end
73
73
 
74
74
  # Includeable version of {Infoboxer.wiki}
75
- def wiki(api_url, options = {})
76
- wikis[api_url] ||= MediaWiki.new(api_url, options || {})
75
+ def wiki(api_url, **options)
76
+ wikis[api_url] ||= MediaWiki.new(api_url, options)
77
77
  end
78
78
 
79
79
  class << self
@@ -168,7 +168,7 @@ module Infoboxer
168
168
  end
169
169
 
170
170
  WIKIMEDIA_PROJECTS.each do |name, domain|
171
- define_method name do |lang = 'en', options = {}|
171
+ define_method name do |lang = 'en', **options|
172
172
  lang, options = 'en', lang if lang.is_a?(Hash)
173
173
 
174
174
  wiki("https://#{lang}.#{domain}/w/api.php", options)
@@ -178,7 +178,7 @@ module Infoboxer
178
178
  alias_method :wp, :wikipedia
179
179
 
180
180
  WIKIMEDIA_COMMONS.each do |name, domain|
181
- define_method name do |options = {}|
181
+ define_method name do |**options|
182
182
  wiki("https://#{domain}/w/api.php", options)
183
183
  end
184
184
  end
@@ -47,15 +47,14 @@ module Infoboxer
47
47
  # for it, as well as shortcuts for some well-known wikis, like
48
48
  # {Infoboxer.wikipedia}.
49
49
  #
50
- # @param api_base_url URL of `api.php` file in your MediaWiki
50
+ # @param api_base_url [String] URL of `api.php` file in your MediaWiki
51
51
  # installation. Typically, its `<domain>/w/api.php`, but can vary
52
52
  # in different wikis.
53
- # @param options Only one option is currently supported:
54
- # * `:user_agent` (also aliased as `:ua`) -- custom User-Agent header.
55
- def initialize(api_base_url, options = {})
53
+ # @param user_agent [String] (also aliased as `:ua`) Custom User-Agent header.
54
+ def initialize(api_base_url, ua: nil, user_agent: ua)
56
55
  @api_base_url = Addressable::URI.parse(api_base_url)
57
- @client = MediaWiktory::Wikipedia::Api.new(api_base_url, user_agent: user_agent(options))
58
- @traits = Traits.get(@api_base_url.host, namespaces: extract_namespaces)
56
+ @client = MediaWiktory::Wikipedia::Api.new(api_base_url, user_agent: user_agent(user_agent))
57
+ @traits = Traits.get(@api_base_url.host, siteinfo)
59
58
  end
60
59
 
61
60
  # Receive "raw" data from Wikipedia (without parsing or wrapping in
@@ -123,7 +122,9 @@ module Infoboxer
123
122
  # and obtain meaningful results instead of `NoMethodError` or
124
123
  # `SomethingNotFound`.
125
124
  #
126
- def get(*titles, prop: [])
125
+ def get(*titles, prop: [], interwiki: nil)
126
+ return interwikis(interwiki).get(*titles, prop: prop) if interwiki
127
+
127
128
  pages = get_h(*titles, prop: prop).values.compact
128
129
  titles.count == 1 ? pages.first : Tree::Nodes[*pages]
129
130
  end
@@ -251,17 +252,26 @@ module Infoboxer
251
252
  [namespace, titl].join(':')
252
253
  end
253
254
 
254
- def user_agent(options)
255
- options[:user_agent] || options[:ua] || self.class.user_agent || UA
255
+ def user_agent(custom)
256
+ custom || self.class.user_agent || UA
257
+ end
258
+
259
+ def siteinfo
260
+ @siteinfo ||= @client.query.meta(:siteinfo).prop(:namespaces, :namespacealiases, :interwikimap).response.to_h
256
261
  end
257
262
 
258
- def extract_namespaces
259
- siteinfo = @client.query.meta(:siteinfo).prop(:namespaces, :namespacealiases).response
260
- siteinfo['namespaces'].map do |_, namespace|
261
- aliases =
262
- siteinfo['namespacealiases'].select { |a| a['id'] == namespace['id'] }.map { |a| a['*'] }
263
- namespace.merge('aliases' => aliases)
264
- end
263
+ def interwikis(prefix)
264
+ @interwikis ||= Hash.new { |h, pre|
265
+ interwiki = siteinfo['interwikimap'].detect { |iw| iw['prefix'] == prefix } or
266
+ fail ArgumentError, "Undefined interwiki: #{prefix}"
267
+
268
+ # FIXME: fragile, but what can we do?..
269
+ m = interwiki['url'].match(%r{^(.+)/wiki/\$1$}) or
270
+ fail ArgumentError, "Interwiki #{interwiki} seems not to be a MediaWiki instance"
271
+ h[pre] = self.class.new("#{m[1]}/w/api.php") # TODO: copy useragent
272
+ }
273
+
274
+ @interwikis[prefix]
265
275
  end
266
276
  end
267
277
  end
@@ -34,9 +34,8 @@ module Infoboxer
34
34
  end
35
35
 
36
36
  # @private
37
- def get(domain, options = {})
38
- cls = Traits.domains[domain]
39
- cls ? cls.new(options) : Traits.new(options)
37
+ def get(domain, site_info = {})
38
+ (Traits.domains[domain] || Traits).new(site_info)
40
39
  end
41
40
 
42
41
  # @private
@@ -68,18 +67,27 @@ module Infoboxer
68
67
  alias_method :default, :new
69
68
  end
70
69
 
71
- def initialize(options = {})
72
- @options = options
73
- @file_namespace =
74
- [DEFAULTS[:file_namespace], namespace_aliases(options, 'File')]
75
- .flatten.compact.uniq
76
- @category_namespace =
77
- [DEFAULTS[:category_namespace], namespace_aliases(options, 'Category')]
78
- .flatten.compact.uniq
70
+ def initialize(site_info = {})
71
+ @site_info = site_info
72
+ end
73
+
74
+ def namespace?(prefix)
75
+ known_namespaces.include?(prefix)
76
+ end
77
+
78
+ def interwiki?(prefix)
79
+ known_interwikis.key?(prefix)
80
+ end
81
+
82
+ # @private
83
+ def file_namespace
84
+ @file_namespace ||= ns_aliases('File')
79
85
  end
80
86
 
81
87
  # @private
82
- attr_reader :file_namespace, :category_namespace
88
+ def category_namespace
89
+ @category_namespace ||= ns_aliases('Category')
90
+ end
83
91
 
84
92
  # @private
85
93
  def templates
@@ -88,16 +96,54 @@ module Infoboxer
88
96
 
89
97
  private
90
98
 
91
- def namespace_aliases(options, canonical)
92
- namespace = (options[:namespaces] || []).detect { |v| v['canonical'] == canonical }
93
- return nil unless namespace
94
- [namespace['*'], *namespace['aliases']]
99
+ def known_namespaces
100
+ @known_namespaces ||=
101
+ if @site_info.empty?
102
+ STANDARD_NAMESPACES
103
+ else
104
+ (@site_info['namespaces'].values + @site_info['namespacealiases']).map { |n| n['*'] }
105
+ end
106
+ end
107
+
108
+ def known_interwikis
109
+ @known_interwikis ||=
110
+ if @site_info.empty?
111
+ {}
112
+ else
113
+ @site_info['interwikimap'].map { |iw| [iw['prefix'], iw] }.to_h
114
+ end
115
+ end
116
+
117
+ def ns_aliases(base)
118
+ return [base] if @site_info.empty?
119
+ main = @site_info['namespaces'].values.detect { |n| n['canonical'] == base }
120
+ [base, main['*']] +
121
+ @site_info['namespacealiases']
122
+ .select { |a| a['id'] == main['id'] }.flat_map { |n| n['*'] }
123
+ .compact.uniq
95
124
  end
96
125
 
97
- DEFAULTS = {
98
- file_namespace: 'File',
99
- category_namespace: 'Category'
100
- }.freeze
126
+ # See https://www.mediawiki.org/wiki/Help:Namespaces#Standard_namespaces
127
+ STANDARD_NAMESPACES = [
128
+ 'Media', # Direct linking to media files.
129
+ 'Special', # Special (non-editable) pages.
130
+ '', # (Main)
131
+ 'Talk', # Article discussion.
132
+ 'User', #
133
+ 'User talk', #
134
+ 'Project', # Meta-discussions related to the operation and development of the wiki.
135
+ 'Project talk', #
136
+ 'File', # Metadata for images, videos, sound files and other media.
137
+ 'File talk', #
138
+ 'MediaWiki', # System messages and other important content.
139
+ 'MediaWiki talk', #
140
+ 'Template', # Templates: blocks of text or wikicode that are intended to be transcluded.
141
+ 'Template talk', #
142
+ 'Help', # Help files, instructions and "how-to" guides.
143
+ 'Help talk', #
144
+ 'Category', # Categories: dynamic lists of other pages.
145
+ 'Category talk', #
146
+ ].freeze
101
147
  end
102
148
  end
103
149
  end
@@ -98,9 +98,13 @@ module Infoboxer
98
98
  # Selects matching nodes from current node's siblings, which
99
99
  # are above current node in parents children list.
100
100
 
101
+ # @!method lookup_prev_sibling(*selectors, &block)
102
+ # Selects first matching nodes from current node's siblings, which
103
+ # are above current node in parents children list.
104
+
101
105
  # Underscored version of {#matches?}
102
106
  def _matches?(selector)
103
- selector.matches?(self)
107
+ selector === self
104
108
  end
105
109
 
106
110
  # Underscored version of {#lookup}
@@ -136,6 +140,11 @@ module Infoboxer
136
140
  prev_siblings._find(selector)
137
141
  end
138
142
 
143
+ # Underscored version of {#lookup_prev_sibling}
144
+ def _lookup_prev_sibling(selector)
145
+ prev_siblings.reverse.detect { |n| selector === n }
146
+ end
147
+
139
148
  # Underscored version of {#lookup_next_siblings}
140
149
  def _lookup_next_siblings(selector)
141
150
  next_siblings._find(selector)
@@ -146,6 +155,7 @@ module Infoboxer
146
155
  lookup lookup_children lookup_parents
147
156
  lookup_siblings
148
157
  lookup_next_siblings lookup_prev_siblings
158
+ lookup_prev_sibling
149
159
  ]
150
160
  .map { |sym| [sym, :"_#{sym}"] }
151
161
  .each do |sym, underscored|
@@ -123,21 +123,25 @@ module Infoboxer
123
123
  #
124
124
  # @return {Tree::Nodes<Section>}
125
125
  def in_sections
126
- main_node = parent.is_a?(Tree::Document) ? self : lookup_parents[-2]
126
+ return parent.in_sections unless parent.is_a?(Tree::Document)
127
+ return @in_sections if @in_sections
127
128
 
128
129
  heading =
129
- if main_node.is_a?(Tree::Heading)
130
- main_node.lookup_prev_siblings(Tree::Heading, level: main_node.level - 1).last
130
+ if is_a?(Tree::Heading)
131
+ lookup_prev_sibling(Tree::Heading, level: level - 1)
131
132
  else
132
- main_node.lookup_prev_siblings(Tree::Heading).last
133
+ lookup_prev_sibling(Tree::Heading)
133
134
  end
134
- return Tree::Nodes[] unless heading
135
+ unless heading
136
+ @in_sections = Tree::Nodes[]
137
+ return @in_sections
138
+ end
135
139
 
136
140
  body = heading.next_siblings
137
141
  .take_while { |n| !n.is_a?(Tree::Heading) || n.level < heading.level }
138
142
 
139
143
  section = Section.new(heading, body)
140
- Tree::Nodes[section, *heading.in_sections]
144
+ @in_sections = Tree::Nodes[section, *heading.in_sections]
141
145
  end
142
146
  end
143
147
 
@@ -24,8 +24,8 @@ module Infoboxer
24
24
  "#<Selector(#{@arg.map(&:to_s).join(', ')})>"
25
25
  end
26
26
 
27
- def matches?(node)
28
- @arg.all? { |a| arg_matches?(a, node) }
27
+ def ===(other)
28
+ @arg.all? { |a| arg_matches?(a, other) }
29
29
  end
30
30
 
31
31
  private
@@ -44,8 +44,8 @@ module Infoboxer
44
44
  check.call(node)
45
45
  when Hash
46
46
  check.all? { |attr, value|
47
- node.respond_to?(attr) && value === node.send(attr) ||
48
- node.params.key?(attr) && value === node.params[attr]
47
+ node.respond_to?(attr) && value_matches?(value, node.send(attr)) ||
48
+ node.params.key?(attr) && value_matches?(value, node.params[attr])
49
49
  }
50
50
  when Symbol
51
51
  node.respond_to?(check) && node.send(check)
@@ -53,6 +53,14 @@ module Infoboxer
53
53
  check === node
54
54
  end
55
55
  end
56
+
57
+ def value_matches?(matcher, value)
58
+ if matcher.is_a?(String) && value.is_a?(String)
59
+ matcher.casecmp(value).zero?
60
+ else
61
+ matcher === value
62
+ end
63
+ end
56
64
  end
57
65
  end
58
66
  end
@@ -83,7 +83,7 @@ module Infoboxer
83
83
 
84
84
  private
85
85
 
86
- def inline_formatting(match)
86
+ def inline_formatting(match) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
87
87
  case match
88
88
  when "'''''"
89
89
  BoldItalic.new(short_inline(/'''''/))
@@ -109,6 +109,8 @@ module Infoboxer
109
109
  reference(Regexp.last_match(1))
110
110
  when /<math>/
111
111
  math
112
+ when /<gallery([^>]*)>/
113
+ gallery(Regexp.last_match(1))
112
114
  when '<'
113
115
  html || Text.new(match) # it was not HTML, just accidental <
114
116
  else
@@ -126,8 +128,18 @@ module Infoboxer
126
128
  caption = inline(/\]\]/)
127
129
  @context.pop_eol_sign
128
130
  end
131
+ name, namespace = link.split(':', 2).reverse
132
+ lnk, params =
133
+ if @context.traits.namespace?(namespace)
134
+ [link, {namespace: namespace}]
135
+ elsif @context.traits.interwiki?(namespace)
136
+ [name, {interwiki: namespace}]
137
+ else
138
+ [link, {}]
139
+ end
129
140
 
130
- Wikilink.new(link, caption)
141
+ puts @context.rest if lnk.nil?
142
+ Wikilink.new(lnk, caption, **params)
131
143
  end
132
144
 
133
145
  # http://en.wikipedia.org/wiki/Help:Link#External_links
@@ -159,6 +171,34 @@ module Infoboxer
159
171
  Text.new(@context.scan_continued_until(%r{</nowiki>}))
160
172
  end
161
173
  end
174
+
175
+ def gallery(tag_rest)
176
+ params = parse_params(tag_rest)
177
+ images = []
178
+ guarded_loop do
179
+ @context.next! if @context.eol?
180
+ path = @context.scan_until(%r{</gallery>|\||$})
181
+ attrs = @context.matched == '|' ? gallery_image_attrs : {}
182
+ unless path.empty?
183
+ images << Tree::Image.new(path.sub(/^#{re.file_namespace}/, ''), attrs)
184
+ end
185
+ break if @context.matched == '</gallery>'
186
+ end
187
+ Gallery.new(images, params)
188
+ end
189
+
190
+ def gallery_image_attrs
191
+ nodes = []
192
+
193
+ guarded_loop do
194
+ nodes << short_inline(%r{\||</gallery>})
195
+ break if @context.eol? || @context.matched?(%r{</gallery>})
196
+ end
197
+
198
+ nodes.map(&method(:image_attr))
199
+ .inject(&:merge)
200
+ .reject { |_k, v| v.nil? || v.empty? }
201
+ end
162
202
  end
163
203
 
164
204
  require_relative 'image'
@@ -14,7 +14,7 @@ module Infoboxer
14
14
 
15
15
  @context.next!
16
16
  end
17
- nodes.flow_templates
17
+ nodes
18
18
  end
19
19
 
20
20
  private
@@ -29,8 +29,8 @@ module Infoboxer
29
29
 
30
30
  guarded_loop do
31
31
  @context.next! while @context.eol?
32
- if @context.check(/\s*([^ =}|<]+)\s*=\s*/)
33
- name = @context.scan(/\s*([^ =]+)/).strip
32
+ if @context.check(/\s*([^=}|<]+)\s*=\s*/)
33
+ name = @context.scan(/\s*([^=]+)/).strip
34
34
  @context.skip(/\s*=\s*/)
35
35
  else
36
36
  name = num
@@ -52,7 +52,7 @@ module Infoboxer
52
52
  end
53
53
 
54
54
  def sanitize_value(nodes)
55
- nodes.pop if nodes.last.is_a?(Pre) && nodes.last.text =~ /^\s*$/ # FIXME: dirty!
55
+ nodes.pop if (nodes.last.is_a?(Pre) || nodes.last.is_a?(Text)) && nodes.last.text =~ /^\s*$/ # FIXME: dirty!
56
56
  nodes
57
57
  end
58
58
  end
@@ -12,6 +12,7 @@ module Infoboxer
12
12
  \[[a-z]+:// | # external link
13
13
  <nowiki[^>]*> | # nowiki
14
14
  <ref[^>]*> | # reference
15
+ <gallery[^>]*>| # gallery
15
16
  <math> | # math
16
17
  < # HTML tag
17
18
  ))x
@@ -63,7 +63,7 @@ module Infoboxer
63
63
  require_relative 'tree/nodes'
64
64
 
65
65
  %w[text compound inline
66
- image html paragraphs list template table ref math
66
+ image gallery html paragraphs list template table ref math
67
67
  document].each do |type|
68
68
  require_relative "tree/#{type}"
69
69
  end
@@ -4,7 +4,7 @@ module Infoboxer
4
4
  module Tree
5
5
  # Base class for all nodes with children.
6
6
  class Compound < Node
7
- def initialize(children = Nodes.new, params = {})
7
+ def initialize(children = Nodes.new, **params)
8
8
  super(params)
9
9
  @children = Nodes[*children]
10
10
  @children.each { |c| c.parent = self }
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ module Infoboxer
4
+ module Tree
5
+ # Represents gallery of images (contents of `<gallery>` special tag).
6
+ #
7
+ # See [Wikipedia Tutorial](https://en.wikipedia.org/wiki/Help:Gallery_tag)
8
+ # for explanation of attributes.
9
+ class Gallery < Compound
10
+ end
11
+ end
12
+ end
@@ -7,8 +7,8 @@ module Infoboxer
7
7
  # See [Wikipedia Tutorial](https://en.wikipedia.org/wiki/Wikipedia:Extended_image_syntax)
8
8
  # for explanation of attributes.
9
9
  class Image < Node
10
- def initialize(path, params = {})
11
- @caption = params.delete(:caption)
10
+ def initialize(path, caption: nil, **params)
11
+ @caption = caption
12
12
  super({path: path}.merge(params))
13
13
  end
14
14
 
@@ -17,8 +17,8 @@ module Infoboxer
17
17
 
18
18
  # Base class for internal/external links,
19
19
  class Link < Compound
20
- def initialize(link, label = nil)
21
- super(label || Nodes.new([Text.new(link)]), link: link)
20
+ def initialize(link, label = nil, **attr)
21
+ super(label || Nodes.new([Text.new(link)]), link: link, **attr)
22
22
  end
23
23
 
24
24
  # @!attribute [r] link
@@ -15,7 +15,7 @@ module Infoboxer
15
15
  # * {Tree::Nodes#follow} for extracting multiple links at once;
16
16
  # * {MediaWiki#get} for basic information on page extraction.
17
17
  def follow
18
- client.get(link)
18
+ client.get(link, interwiki: interwiki)
19
19
  end
20
20
 
21
21
  # Human-readable page URL
@@ -28,6 +28,9 @@ module Infoboxer
28
28
 
29
29
  protected
30
30
 
31
+ # redefined in {Wikilink}
32
+ def interwiki; end
33
+
31
34
  def page
32
35
  lookup_parents(MediaWiki::Page).first or fail('Not in a page from real source')
33
36
  end
@@ -4,9 +4,6 @@ module Infoboxer
4
4
  #
5
5
  # See also: https://en.wikipedia.org/wiki/Help:Displaying_a_formula
6
6
  class Math < Text
7
- def text
8
- "<math>#{super}</math>"
9
- end
10
7
  end
11
8
  end
12
9
  end
@@ -11,7 +11,7 @@ module Infoboxer
11
11
  # you will receive it from tree and use for navigations.
12
12
  #
13
13
  class Node
14
- def initialize(params = {})
14
+ def initialize(**params)
15
15
  @params = params
16
16
  end
17
17
 
@@ -154,7 +154,7 @@ module Infoboxer
154
154
  end
155
155
 
156
156
  def show_params(prms = nil)
157
- (prms || params).map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
157
+ (prms || params).reject { |_, v| v.nil? }.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
158
158
  end
159
159
 
160
160
  def indent(level)
@@ -38,10 +38,19 @@ module Infoboxer
38
38
  # @!method compact
39
39
  # Just like Array#compact, but returns Nodes
40
40
 
41
+ # @!method grep(pattern)
42
+ # Just like Array#grep, but returns Nodes
43
+
44
+ # @!method grep_v(pattern)
45
+ # Just like Array#grep_v, but returns Nodes
46
+
41
47
  # @!method -(other)
42
48
  # Just like Array#-, but returns Nodes
43
49
 
44
- %i[select reject sort_by flatten compact -].each do |sym|
50
+ # @!method +(other)
51
+ # Just like Array#+, but returns Nodes
52
+
53
+ %i[select reject sort_by flatten compact grep grep_v - +].each do |sym|
45
54
  define_method(sym) do |*args, &block|
46
55
  Nodes[*super(*args, &block)]
47
56
  end
@@ -75,6 +84,21 @@ module Infoboxer
75
84
  end
76
85
  end
77
86
 
87
+ # Just like Array#flat_map, but returns Nodes, **if** all map results are Node
88
+ def flat_map
89
+ res = super
90
+ if res.all? { |n| n.is_a?(Node) || n.is_a?(Nodes) }
91
+ Nodes[*res]
92
+ else
93
+ res
94
+ end
95
+ end
96
+
97
+ # Just like Array#group, but returns hash with `{<grouping variable> => Nodes}`
98
+ def group_by
99
+ super.map { |title, group| [title, Nodes[*group]] }.to_h
100
+ end
101
+
78
102
  # @!method prev_siblings
79
103
  # Previous siblings (flat list) of all nodes inside.
80
104
 
@@ -139,12 +163,14 @@ module Infoboxer
139
163
  # @return [Nodes<MediaWiki::Page>] It is still `Nodes`, so you
140
164
  # still can process them uniformely.
141
165
  def follow
142
- links = select { |n| n.respond_to?(:link) }.map(&:link)
166
+ links = grep(Linkable)
143
167
  return Nodes[] if links.empty?
144
168
  page = first.lookup_parents(MediaWiki::Page).first or
145
169
  fail('Not in a page from real source')
146
170
  page.client or fail('MediaWiki client not set')
147
- page.client.get(*links)
171
+ pages = links.group_by(&:interwiki)
172
+ .flat_map { |iw, ls| page.client.get(*ls.map(&:link), interwiki: iw) }
173
+ pages.count == 1 ? pages.first : Nodes[*pages]
148
174
  end
149
175
 
150
176
  # @private
@@ -173,7 +199,9 @@ module Infoboxer
173
199
  # @private
174
200
  # Internal, used by {Parser}
175
201
  def flow_templates
176
- make_nodes(map { |n| n.is_a?(Paragraph) ? n.to_templates? : n })
202
+ # TODO: will it be better?..
203
+ # make_nodes(map { |n| n.is_a?(Paragraph) ? n.to_templates? : n })
204
+ self
177
205
  end
178
206
 
179
207
  private
@@ -22,6 +22,10 @@ module Infoboxer
22
22
  false
23
23
  end
24
24
 
25
+ def named?
26
+ name !~ /^\d+$/
27
+ end
28
+
25
29
  protected
26
30
 
27
31
  def descr
@@ -139,7 +143,7 @@ module Infoboxer
139
143
  #
140
144
  # @return [Nodes<Var>]
141
145
  def unnamed_variables
142
- variables.find(name: /^\d+$/)
146
+ variables.reject(&:named?)
143
147
  end
144
148
 
145
149
  # Fetches template variable(s) by name(s) or patterns.
@@ -242,7 +246,7 @@ module Infoboxer
242
246
  def extract_params(vars)
243
247
  vars
244
248
  .select { |v| v.children.count == 1 && v.children.first.is_a?(Text) }
245
- .map { |v| [v.name, v.children.first.raw_text] }.to_h
249
+ .map { |v| [v.name.to_sym, v.children.first.raw_text] }.to_h
246
250
  end
247
251
 
248
252
  def inspect_variables(depth)
@@ -15,7 +15,7 @@ module Infoboxer
15
15
  # Text fragment without decodint of HTML entities.
16
16
  attr_accessor :raw_text
17
17
 
18
- def initialize(text, params = {})
18
+ def initialize(text, **params)
19
19
  super(params)
20
20
  @raw_text = text
21
21
  end
@@ -12,14 +12,23 @@ module Infoboxer
12
12
  # Note, that Wikilink is {Linkable}, so you can {Linkable#follow #follow}
13
13
  # it to obtain linked pages.
14
14
  class Wikilink < Link
15
- def initialize(*)
16
- super
17
- parse_link!
15
+ def initialize(link, label = nil, namespace: nil, interwiki: nil)
16
+ super(link, label, namespace: namespace, interwiki: interwiki)
17
+ @namespace = namespace || ''
18
+ @interwiki = interwiki
19
+ parse_name!
18
20
  end
19
21
 
20
22
  # "Clean" wikilink name, for ex., `Cities` for `[Category:Cities]`
21
23
  attr_reader :name
22
24
 
25
+ # Interwiki identifier. For example, `[[wikt:Argentina]]`
26
+ # will have `"Argentina"` as its {#name} and `"wikt"` (wiktionary) as an
27
+ # interwiki. TODO: how to use it.
28
+ #
29
+ # See [Wikipedia docs](https://en.wikipedia.org/wiki/Help:Interwiki_linking) for details.
30
+ attr_reader :interwiki
31
+
23
32
  # Wikilink namespace, `Category` for `[Category:Cities]`, empty
24
33
  # string (not `nil`!) for just `[Cities]`
25
34
  attr_reader :namespace
@@ -46,10 +55,8 @@ module Infoboxer
46
55
 
47
56
  private
48
57
 
49
- def parse_link!
50
- @name, @namespace = link.split(':', 2).reverse
51
- @namespace ||= ''
52
-
58
+ def parse_name!
59
+ @name = namespace.empty? ? link : link.sub(/^#{namespace}:/, '')
53
60
  @name, @anchor = @name.split('#', 2)
54
61
  @anchor ||= ''
55
62
 
@@ -3,6 +3,7 @@
3
3
  module Infoboxer
4
4
  MAJOR = 0
5
5
  MINOR = 3
6
- PATCH = 0
7
- VERSION = [MAJOR, MINOR, PATCH].join('.')
6
+ PATCH = 1
7
+ PRE = 'pre'.freeze # set to `nil` for normal releases
8
+ VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join('.')
8
9
  end
@@ -36,7 +36,7 @@ module Infoboxer
36
36
  attrs[attr.to_sym] = process_value(value)
37
37
  end
38
38
  res = op == '//' ? {op: :lookup} : {}
39
- res[:type] = type.gsub(/(?:^|_)([a-z])/, &:upcase).tr('_', '').to_sym unless type.empty?
39
+ res[:type] = process_type(type) unless type.empty?
40
40
  res.merge(attrs) # TODO: raise if empty selector
41
41
  end
42
42
 
@@ -51,6 +51,15 @@ module Infoboxer
51
51
  end
52
52
  end
53
53
 
54
+ def process_type(type)
55
+ type.gsub(/(?:^|_)([a-z])/, &:upcase).tr('_', '').to_sym
56
+ .tap { |t| valid_type?(t) or fail(ParseError, "Unrecognized node type: #{type}") }
57
+ end
58
+
59
+ def valid_type?(t)
60
+ t == :Section || Infoboxer::Tree.const_defined?(t)
61
+ end
62
+
54
63
  def unexpected(scanner, expected)
55
64
  place = scanner.eos? ? 'end of pattern' : scanner.rest.inspect
56
65
  fail ParseError, "Unexpected #{place}, expecting #{expected}"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: infoboxer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Shepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-23 00:00:00.000000000 Z
11
+ date: 2017-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: htmlentities
@@ -117,6 +117,7 @@ files:
117
117
  - lib/infoboxer/tree.rb
118
118
  - lib/infoboxer/tree/compound.rb
119
119
  - lib/infoboxer/tree/document.rb
120
+ - lib/infoboxer/tree/gallery.rb
120
121
  - lib/infoboxer/tree/html.rb
121
122
  - lib/infoboxer/tree/image.rb
122
123
  - lib/infoboxer/tree/inline.rb
@@ -165,9 +166,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
165
166
  version: 2.1.0
166
167
  required_rubygems_version: !ruby/object:Gem::Requirement
167
168
  requirements:
168
- - - ">="
169
+ - - ">"
169
170
  - !ruby/object:Gem::Version
170
- version: '0'
171
+ version: 1.3.1
171
172
  requirements: []
172
173
  rubyforge_project:
173
174
  rubygems_version: 2.6.10