micromicro 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/CONTRIBUTING.md +3 -3
  4. data/README.md +9 -9
  5. data/lib/micro_micro/collectible.rb +2 -0
  6. data/lib/micro_micro/collections/base_collection.rb +7 -1
  7. data/lib/micro_micro/collections/items_collection.rb +3 -1
  8. data/lib/micro_micro/collections/properties_collection.rb +12 -0
  9. data/lib/micro_micro/collections/relationships_collection.rb +10 -9
  10. data/lib/micro_micro/document.rb +10 -98
  11. data/lib/micro_micro/helpers.rb +82 -0
  12. data/lib/micro_micro/implied_property.rb +2 -0
  13. data/lib/micro_micro/item.rb +53 -60
  14. data/lib/micro_micro/parsers/base_implied_property_parser.rb +29 -0
  15. data/lib/micro_micro/parsers/base_property_parser.rb +4 -12
  16. data/lib/micro_micro/parsers/date_time_parser.rb +60 -25
  17. data/lib/micro_micro/parsers/date_time_property_parser.rb +7 -6
  18. data/lib/micro_micro/parsers/embedded_markup_property_parser.rb +3 -2
  19. data/lib/micro_micro/parsers/implied_name_property_parser.rb +14 -16
  20. data/lib/micro_micro/parsers/implied_photo_property_parser.rb +19 -43
  21. data/lib/micro_micro/parsers/implied_url_property_parser.rb +11 -30
  22. data/lib/micro_micro/parsers/plain_text_property_parser.rb +3 -1
  23. data/lib/micro_micro/parsers/url_property_parser.rb +20 -12
  24. data/lib/micro_micro/parsers/value_class_pattern_parser.rb +27 -42
  25. data/lib/micro_micro/property.rb +68 -56
  26. data/lib/micro_micro/relationship.rb +15 -13
  27. data/lib/micro_micro/version.rb +3 -1
  28. data/lib/micromicro.rb +31 -26
  29. data/micromicro.gemspec +11 -6
  30. metadata +22 -19
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MicroMicro
4
+ module Parsers
5
+ class BaseImpliedPropertyParser < BasePropertyParser
6
+ private
7
+
8
+ # @return [String, nil]
9
+ def attribute_value
10
+ candidate_node[self.class::HTML_ELEMENTS_MAP[candidate_node.name]] if candidate_node
11
+ end
12
+
13
+ # @return [Nokogiri::XML::Element, nil]
14
+ def candidate_node
15
+ @candidate_node ||=
16
+ candidate_nodes.find do |node|
17
+ self.class::HTML_ELEMENTS_MAP.filter_map do |name, attribute|
18
+ node if name == node.name && node[attribute]
19
+ end.any?
20
+ end
21
+ end
22
+
23
+ # @return [Nokogiri::XML::NodeSet]
24
+ def candidate_nodes
25
+ Nokogiri::XML::NodeSet.new(node.document, child_nodes.unshift(node))
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
5
  class BasePropertyParser
@@ -12,20 +14,10 @@ module MicroMicro
12
14
  #
13
15
  # @return [String]
14
16
  def value
15
- @value ||= begin
16
- Document.text_content_from(node) do |context|
17
+ @value ||=
18
+ Helpers.text_content_from(node) do |context|
17
19
  context.css('img').each { |img| img.content = " #{img['alt'] || img['src']} " }
18
20
  end
19
- end
20
- end
21
-
22
- # @param node [Nokogiri::XML::Element]
23
- # @param attributes_map [Hash{String => Array}]
24
- # @return [Array]
25
- def self.attribute_value_from(node, attributes_map)
26
- attributes_map.map do |attribute, names|
27
- node[attribute] if names.include?(node.name) && node[attribute]
28
- end.compact.first
29
21
  end
30
22
 
31
23
  private
@@ -1,78 +1,113 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
5
  class DateTimeParser
4
- # @see https://microformats.org/wiki/value-class-pattern#Date_and_time_parsing
5
- #
6
6
  # Regexp pattern matching YYYY-MM-DD and YYY-DDD
7
- DATE_REGEXP_PATTERN = '(?<year>\d{4})-((?<ordinal>3[0-6]{2}|[0-2]\d{2})|(?<month>0\d|1[0-2])-(?<day>3[0-1]|[0-2]\d))'.freeze
8
- # Regexp pattern matching HH:MM and HH:MM:SS
9
- TIME_REGEXP_PATTERN = '(?<hours>2[0-3]|[0-1]?\d)(?::(?<minutes>[0-5]\d))?(?::(?<seconds>[0-5]\d))?(?:\s*?(?<abbreviation>[apPP]\.?[mM]\.?))?'.freeze
10
- # Regexp pattern matching +/-(XX:YY|XXYY|XX) or the literal string Z
11
- TIMEZONE_REGEXP_PATTERN = '(?<zulu>Z)|(?<offset>(?:\+|-)(?:1[0-2]|0?\d)(?::?[0-5]\d)?)'.freeze
7
+ DATE_REGEXP_PATTERN = '(?<year>\d{4})-' \
8
+ '((?<ordinal>3[0-6]{2}|[0-2]\d{2})|(?<month>0\d|1[0-2])-' \
9
+ '(?<day>3[0-1]|[0-2]\d))'
12
10
 
13
- CAPTURE_NAMES = [:year, :ordinal, :month, :day, :hours, :minutes, :seconds, :abbreviation, :zulu, :offset].freeze
11
+ # Regexp pattern matching HH:MM and HH:MM:SS
12
+ TIME_REGEXP_PATTERN = '(?<hours>2[0-3]|[0-1]?\d)' \
13
+ '(?::(?<minutes>[0-5]\d))?' \
14
+ '(?::(?<seconds>[0-5]\d))?' \
15
+ '(?:\s*?(?<abbreviation>[apPP]\.?[mM]\.?))?'
14
16
 
15
- # @param string [String]
17
+ # Regexp pattern matching +/-(XX:YY|XXYY|XX) or the literal string Z
18
+ TIMEZONE_REGEXP_PATTERN = '(?<zulu>Z)|(?<offset>(?:\+|-)(?:1[0-2]|0?\d)(?::?[0-5]\d)?)'
19
+
20
+ # Regexp for extracting named captures from a datetime-esque String.
21
+ DATE_TIME_TIMEZONE_REGEXP = /
22
+ \A
23
+ (?=.)
24
+ (?:#{DATE_REGEXP_PATTERN})?
25
+ (?:\s?#{TIME_REGEXP_PATTERN}(?:#{TIMEZONE_REGEXP_PATTERN})?)?
26
+ \z
27
+ /x.freeze
28
+
29
+ # Parse a string for date and/or time values according to the Microformats
30
+ # Value Class Pattern date and time parsing specification.
31
+ #
32
+ # @see https://microformats.org/wiki/value-class-pattern#Date_and_time_parsing
33
+ #
34
+ # @param string [String, #to_s]
16
35
  def initialize(string)
17
- @string = string
36
+ @string = string.to_s
18
37
  end
19
38
 
20
- CAPTURE_NAMES.each do |name|
39
+ # Define getter and predicate methods for all possible named captures
40
+ # returned by the DATE_TIME_TIMEZONE_REGEXP regular expression.
41
+ [
42
+ :year, :ordinal, :month, :day,
43
+ :hours, :minutes, :seconds,
44
+ :abbreviation, :zulu, :offset
45
+ ].each do |name|
21
46
  define_method(name) { values[name] }
22
47
  define_method("#{name}?") { public_send(name).present? }
23
48
  end
24
49
 
50
+ # @return [String, nil]
25
51
  def normalized_calendar_date
26
52
  @normalized_calendar_date ||= "#{year}-#{month}-#{day}" if year? && month? && day?
27
53
  end
28
54
 
55
+ # @return [String, nil]
29
56
  def normalized_date
30
57
  @normalized_date ||= normalized_calendar_date || normalized_ordinal_date
31
58
  end
32
59
 
60
+ # @return [String, nil]
33
61
  def normalized_hours
34
- @normalized_hours ||= begin
35
- return unless hours?
36
- return (hours.to_i + 12).to_s if abbreviation&.tr('.', '')&.downcase == 'pm'
62
+ @normalized_hours ||=
63
+ if hours?
64
+ return (hours.to_i + 12).to_s if abbreviation&.tr('.', '')&.downcase == 'pm'
37
65
 
38
- format('%<hours>02d', hours: hours)
39
- end
66
+ format('%<hours>02d', hours: hours)
67
+ end
40
68
  end
41
69
 
70
+ # @return [String]
42
71
  def normalized_minutes
43
72
  @normalized_minutes ||= minutes || '00'
44
73
  end
45
74
 
75
+ # @return [String, nil]
46
76
  def normalized_ordinal_date
47
77
  @normalized_ordinal_date ||= "#{year}-#{ordinal}" if year? && ordinal?
48
78
  end
49
79
 
80
+ # @return [String, nil]
50
81
  def normalized_time
51
82
  @normalized_time ||= [normalized_hours, normalized_minutes, seconds].compact.join(':') if normalized_hours
52
83
  end
53
84
 
85
+ # @return [String, nil]
54
86
  def normalized_timezone
55
87
  @normalized_timezone ||= zulu || offset&.tr(':', '')
56
88
  end
57
89
 
58
- # @return [String]
90
+ # @return [String, nil]
59
91
  def value
60
- @value ||= "#{normalized_date} #{normalized_time}#{normalized_timezone}".strip if normalized_date || normalized_time || normalized_timezone
92
+ @value ||=
93
+ if normalized_date || normalized_time || normalized_timezone
94
+ "#{normalized_date} #{normalized_time}#{normalized_timezone}".strip
95
+ end
61
96
  end
62
97
 
63
98
  # @return [Hash{Symbol => String, nil}]
64
99
  def values
65
- @values ||= self.class.values_from(string)
66
- end
67
-
68
- # @param string [String]
69
- # @return [Hash{Symbol => String, nil}]
70
- def self.values_from(string)
71
- string&.match(/^(?:#{DATE_REGEXP_PATTERN})?(?:\s?#{TIME_REGEXP_PATTERN}(?:#{TIMEZONE_REGEXP_PATTERN})?)?$/)&.named_captures.to_h.symbolize_keys
100
+ @values ||=
101
+ if string.match?(DATE_TIME_TIMEZONE_REGEXP)
102
+ string.match(DATE_TIME_TIMEZONE_REGEXP).named_captures.symbolize_keys
103
+ else
104
+ {}
105
+ end
72
106
  end
73
107
 
74
108
  private
75
109
 
110
+ # @return [String]
76
111
  attr_reader :string
77
112
  end
78
113
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
5
  class DateTimePropertyParser < BasePropertyParser
@@ -20,16 +22,15 @@ module MicroMicro
20
22
  #
21
23
  # @return [MicroMicro::Parsers::DateTimeParser, nil]
22
24
  def adopted_date_time_parser
23
- @adopted_date_time_parser ||= begin
24
- date_time_siblings = (property.prev_all.reverse + property.next_all).select { |prop| prop.prefix == 'dt' }
25
-
26
- date_time_siblings.map { |prop| DateTimeParser.new(prop.value) }.find(&:normalized_date)
27
- end
25
+ @adopted_date_time_parser ||=
26
+ (property.prev_all.reverse + property.next_all).filter_map do |prop|
27
+ DateTimeParser.new(prop.value) if prop.date_time_property?
28
+ end.find(&:normalized_date)
28
29
  end
29
30
 
30
31
  # @return [String, nil]
31
32
  def attribute_value
32
- self.class.attribute_value_from(node, HTML_ATTRIBUTES_MAP)
33
+ Helpers.attribute_value_from(node, HTML_ATTRIBUTES_MAP)
33
34
  end
34
35
 
35
36
  # @return [MicroMicro::Parsers::DateTimeParser]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
5
  class EmbeddedMarkupPropertyParser < BasePropertyParser
@@ -5,12 +7,11 @@ module MicroMicro
5
7
  #
6
8
  # @return [Hash{Symbol => String}]
7
9
  def value
8
- @value ||= begin
10
+ @value ||=
9
11
  {
10
12
  html: node.inner_html.strip,
11
13
  value: super
12
14
  }
13
- end
14
15
  end
15
16
  end
16
17
  end
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
- class ImpliedNamePropertyParser < BasePropertyParser
4
- HTML_ATTRIBUTES_MAP = {
5
- 'alt' => %w[area img],
6
- 'title' => %w[abbr]
5
+ class ImpliedNamePropertyParser < BaseImpliedPropertyParser
6
+ HTML_ELEMENTS_MAP = {
7
+ 'img' => 'alt',
8
+ 'area' => 'alt',
9
+ 'abbr' => 'title'
7
10
  }.freeze
8
11
 
9
12
  # @see https://microformats.org/wiki/microformats2-parsing#parsing_for_implied_properties
@@ -15,24 +18,19 @@ module MicroMicro
15
18
 
16
19
  private
17
20
 
18
- # @return [Nokogiri::XML::NodeSet]
19
- def candidate_nodes
20
- @candidate_nodes ||= Nokogiri::XML::NodeSet.new(node.document, child_nodes.unshift(node))
21
- end
22
-
23
21
  # @return [Array]
24
22
  def child_nodes
25
- [node.at_css('> :only-child'), node.at_css('> :only-child > :only-child')].compact.reject { |child_node| Item.item_node?(child_node) }
26
- end
27
-
28
- # @return [String, nil]
29
- def attribute_value
30
- candidate_nodes.map { |node| self.class.attribute_value_from(node, HTML_ATTRIBUTES_MAP) }.compact.first
23
+ [
24
+ node.at_css('> :only-child'),
25
+ node.at_css('> :only-child > :only-child')
26
+ ].compact.reject { |child_node| Helpers.item_node?(child_node) }
31
27
  end
32
28
 
33
29
  # @return [String]
34
30
  def text_content
35
- @text_content ||= Document.text_content_from(node) { |context| context.css('img').each { |img| img.content = img['alt'] } }
31
+ Helpers.text_content_from(node) do |context|
32
+ context.css('img').each { |img| img.content = img['alt'] }
33
+ end
36
34
  end
37
35
  end
38
36
  end
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
- class ImpliedPhotoPropertyParser < BasePropertyParser
5
+ class ImpliedPhotoPropertyParser < BaseImpliedPropertyParser
6
+ CSS_SELECTORS_ARRAY = ['> img[src]:only-of-type', '> object[data]:only-of-type'].freeze
7
+
4
8
  HTML_ELEMENTS_MAP = {
5
9
  'img' => 'src',
6
10
  'object' => 'data'
@@ -11,54 +15,26 @@ module MicroMicro
11
15
  #
12
16
  # @return [String, Hash{Symbol => String}, nil]
13
17
  def value
14
- @value ||= begin
15
- return unless resolved_value
16
- return resolved_value unless value_node.matches?('img[alt]')
17
-
18
- {
19
- value: resolved_value,
20
- alt: value_node['alt'].strip
21
- }
22
- end
18
+ @value ||=
19
+ if attribute_value
20
+ return attribute_value unless candidate_node.matches?('img[alt]')
21
+
22
+ {
23
+ value: attribute_value,
24
+ alt: candidate_node['alt'].strip
25
+ }
26
+ end
23
27
  end
24
28
 
25
29
  private
26
30
 
27
- # @return [Array<String>]
28
- def attribute_values
29
- @attribute_values ||= begin
30
- HTML_ELEMENTS_MAP.map do |element, attribute|
31
- node if node.matches?("#{element}[#{attribute}]")
32
- end.compact
33
- end
34
- end
35
-
36
- # @return [String, nil]
37
- def resolved_value
38
- @resolved_value ||= value_node[HTML_ELEMENTS_MAP[value_node.name]] if value_node
39
- end
31
+ # @return [Array]
32
+ def child_nodes
33
+ nodes = [node.at_css(*CSS_SELECTORS_ARRAY)]
40
34
 
41
- # @return [Nokogiri::XML::Element, nil]
42
- def value_node
43
- @value_node ||= begin
44
- return attribute_values.first if attribute_values.any?
45
-
46
- HTML_ELEMENTS_MAP.each do |element, attribute|
47
- child_node = node.at_css("> #{element}[#{attribute}]:only-of-type")
48
-
49
- return child_node if child_node && !Item.item_node?(child_node) && element == child_node.name && child_node[attribute]
50
- end
51
-
52
- if node.element_children.one? && !Item.item_node?(node.first_element_child)
53
- HTML_ELEMENTS_MAP.each do |element, attribute|
54
- child_node = node.first_element_child.at_css("> #{element}[#{attribute}]:only-of-type")
55
-
56
- return child_node if child_node && !Item.item_node?(child_node) && element == child_node.name && child_node[attribute]
57
- end
58
- end
35
+ nodes << node.first_element_child.at_css(*CSS_SELECTORS_ARRAY) if node.element_children.one?
59
36
 
60
- nil
61
- end
37
+ nodes.compact.reject { |child_node| Helpers.item_node?(child_node) }
62
38
  end
63
39
  end
64
40
  end
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
- class ImpliedUrlPropertyParser < BasePropertyParser
5
+ class ImpliedUrlPropertyParser < BaseImpliedPropertyParser
6
+ CSS_SELECTORS_ARRAY = ['> a[href]:only-of-type', '> area[href]:only-of-type'].freeze
7
+
4
8
  HTML_ELEMENTS_MAP = {
5
9
  'a' => 'href',
6
10
  'area' => 'href'
@@ -10,41 +14,18 @@ module MicroMicro
10
14
  #
11
15
  # @return [String, nil]
12
16
  def value
13
- @value ||= value_node[HTML_ELEMENTS_MAP[value_node.name]] if value_node
17
+ @value ||= attribute_value
14
18
  end
15
19
 
16
20
  private
17
21
 
18
- # @return [Array<String>]
19
- def attribute_values
20
- @attribute_values ||= begin
21
- HTML_ELEMENTS_MAP.map do |element, attribute|
22
- node if node.matches?("#{element}[#{attribute}]")
23
- end.compact
24
- end
25
- end
26
-
27
- # @return [Nokogiri::XML::Element, nil]
28
- def value_node
29
- @value_node ||= begin
30
- return attribute_values.first if attribute_values.any?
31
-
32
- HTML_ELEMENTS_MAP.each do |element, attribute|
33
- child_node = node.at_css("> #{element}[#{attribute}]:only-of-type")
34
-
35
- return child_node if child_node && !Item.item_node?(child_node) && element == child_node.name && child_node[attribute]
36
- end
37
-
38
- if node.element_children.one? && !Item.item_node?(node.first_element_child)
39
- HTML_ELEMENTS_MAP.each do |element, attribute|
40
- child_node = node.first_element_child.at_css("> #{element}[#{attribute}]:only-of-type")
22
+ # @return [Array]
23
+ def child_nodes
24
+ nodes = [node.at_css(*CSS_SELECTORS_ARRAY)]
41
25
 
42
- return child_node if child_node && !Item.item_node?(child_node) && element == child_node.name && child_node[attribute]
43
- end
44
- end
26
+ nodes << node.first_element_child.at_css(*CSS_SELECTORS_ARRAY) if node.element_children.one?
45
27
 
46
- nil
47
- end
28
+ nodes.compact.reject { |child_node| Helpers.item_node?(child_node) }
48
29
  end
49
30
  end
50
31
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
5
  class PlainTextPropertyParser < BasePropertyParser
@@ -18,7 +20,7 @@ module MicroMicro
18
20
 
19
21
  # @return [String, nil]
20
22
  def attribute_value
21
- self.class.attribute_value_from(node, HTML_ATTRIBUTES_MAP)
23
+ Helpers.attribute_value_from(node, HTML_ATTRIBUTES_MAP)
22
24
  end
23
25
 
24
26
  # @return [String, nil]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
5
  class UrlPropertyParser < BasePropertyParser
@@ -18,36 +20,42 @@ module MicroMicro
18
20
  #
19
21
  # @return [String, Hash{Symbol => String}]
20
22
  def value
21
- @value ||= begin
22
- return resolved_value unless node.matches?('img[alt]')
23
-
24
- {
25
- value: resolved_value,
26
- alt: node['alt'].strip
27
- }
28
- end
23
+ @value ||=
24
+ if node.matches?('img[alt]')
25
+ {
26
+ value: resolved_value,
27
+ alt: node['alt'].strip
28
+ }
29
+ else
30
+ resolved_value
31
+ end
29
32
  end
30
33
 
31
34
  private
32
35
 
33
36
  # @return [String, nil]
34
37
  def attribute_value
35
- self.class.attribute_value_from(node, HTML_ATTRIBUTES_MAP)
38
+ Helpers.attribute_value_from(node, HTML_ATTRIBUTES_MAP)
36
39
  end
37
40
 
38
41
  # @return [String, nil]
39
42
  def extended_attribute_value
40
- self.class.attribute_value_from(node, EXTENDED_HTML_ATTRIBUTES_MAP)
43
+ Helpers.attribute_value_from(node, EXTENDED_HTML_ATTRIBUTES_MAP)
41
44
  end
42
45
 
43
46
  # @return [String]
44
47
  def resolved_value
45
- @resolved_value ||= Addressable::URI.join(node.document.url, unresolved_value.strip).to_s
48
+ @resolved_value ||= node.document.resolve_relative_url(unresolved_value.strip)
49
+ end
50
+
51
+ # @return [String]
52
+ def text_content
53
+ Helpers.text_content_from(node)
46
54
  end
47
55
 
48
56
  # @return [String]
49
57
  def unresolved_value
50
- attribute_value || value_class_pattern_value || extended_attribute_value || Document.text_content_from(node)
58
+ attribute_value || value_class_pattern_value || extended_attribute_value || text_content
51
59
  end
52
60
 
53
61
  # @return [String, nil]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MicroMicro
2
4
  module Parsers
3
5
  class ValueClassPatternParser
@@ -10,72 +12,55 @@ module MicroMicro
10
12
  'datetime' => %w[del ins time]
11
13
  }.freeze
12
14
 
13
- # @param context [Nokogiri::XML::Element]
14
- # @param separator [String]
15
- def initialize(node, separator = '')
16
- @node = node
17
- @separator = separator
18
- end
19
-
20
- # @return [String, nil]
21
- def value
22
- @value ||= values.join(separator).strip if values.any?
23
- end
24
-
25
- # @return [Array<String>]
26
- def values
27
- @values ||= value_nodes.map { |value_node| self.class.value_from(value_node) }.select(&:present?)
28
- end
29
-
30
15
  # @param context [Nokogiri::XML::NodeSet, Nokogiri::XML::Element]
31
16
  # @param node_set [Nokogiri::XML::NodeSet]
32
17
  # @return [Nokogiri::XML::NodeSet]
33
- def self.nodes_from(context, node_set = Nokogiri::XML::NodeSet.new(context.document, []))
34
- context.each { |node| nodes_from(node, node_set) } if context.is_a?(Nokogiri::XML::NodeSet)
18
+ def self.node_set_from(context, node_set = Nokogiri::XML::NodeSet.new(context.document, []))
19
+ context.each { |node| node_set_from(node, node_set) } if context.is_a?(Nokogiri::XML::NodeSet)
35
20
 
36
- if context.is_a?(Nokogiri::XML::Element) && !Document.ignore_node?(context)
37
- if value_class_node?(context) || value_title_node?(context)
21
+ if context.is_a?(Nokogiri::XML::Element) && !Helpers.ignore_node?(context)
22
+ if Helpers.value_class_node?(context) || Helpers.value_title_node?(context)
38
23
  node_set << context
39
24
  else
40
- nodes_from(context.element_children, node_set)
25
+ node_set_from(context.element_children, node_set)
41
26
  end
42
27
  end
43
28
 
44
29
  node_set
45
30
  end
46
31
 
47
- # @param node [Nokogiri::XML::Element]
48
- # @return [Boolean]
49
- def self.value_class_node?(node)
50
- node.classes.include?('value')
51
- end
52
-
53
32
  # @param node [Nokogiri::XML::Element]
54
33
  # @return [String, nil]
55
34
  def self.value_from(node)
56
- return node['title'] if value_title_node?(node)
35
+ return node['title'] if Helpers.value_title_node?(node)
57
36
 
58
- HTML_ATTRIBUTES_MAP.each do |attribute, names|
59
- return node[attribute] if names.include?(node.name) && node[attribute]
60
- end
37
+ Helpers.attribute_value_from(node, HTML_ATTRIBUTES_MAP) || node.text
38
+ end
61
39
 
62
- node.text
40
+ # @param context [Nokogiri::XML::Element]
41
+ # @param separator [String]
42
+ def initialize(node, separator = '')
43
+ @node = node
44
+ @separator = separator
63
45
  end
64
46
 
65
- # @param node [Nokogiri::XML::Element]
66
- # @return [Boolean]
67
- def self.value_title_node?(node)
68
- node.classes.include?('value-title')
47
+ # @return [String, nil]
48
+ def value
49
+ @value ||= values.join(separator).strip if values.any?
50
+ end
51
+
52
+ # @return [Array<String>]
53
+ def values
54
+ @values ||=
55
+ self.class
56
+ .node_set_from(node)
57
+ .map { |value_node| self.class.value_from(value_node) }
58
+ .select(&:present?)
69
59
  end
70
60
 
71
61
  private
72
62
 
73
63
  attr_reader :node, :separator
74
-
75
- # @return [Nokogiri::XML::NodeSet]
76
- def value_nodes
77
- @value_nodes ||= self.class.nodes_from(node)
78
- end
79
64
  end
80
65
  end
81
66
  end