Class: Sheng::MergeField

Inherits:
Object
  • Object
show all
Defined in:
lib/sheng/merge_field.rb

Defined Under Namespace

Classes: NotAMergeFieldError

Constant Summary collapse

MATH_TOKENS =
%w[+ - / * ( )]
REGEXES =
{
  instruction_text: /^\s*MERGEFIELD(.*)\\\* MERGEFORMAT\s*$/,
  key_string: /^(?<prefix>start:|end:|if:|end_if:|unless:|end_unless:)?\s*(?<key>[^\|]+)\s*\|?(?<filters>.*)?/
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(element) ⇒ MergeField

Returns a new instance of MergeField.



23
24
25
26
27
28
# File 'lib/sheng/merge_field.rb', line 23

def initialize(element)
  @element = element
  @xml_document = element.document
  @instruction_text = Sheng::Support.extract_mergefield_instruction_text(element)
  @errors = []
end

Instance Attribute Details

#elementObject (readonly)

Returns the value of attribute element.



21
22
23
# File 'lib/sheng/merge_field.rb', line 21

def element
  @element
end

#errorsObject (readonly)

Returns the value of attribute errors.



21
22
23
# File 'lib/sheng/merge_field.rb', line 21

def errors
  @errors
end

#xml_documentObject (readonly)

Returns the value of attribute xml_document.



21
22
23
# File 'lib/sheng/merge_field.rb', line 21

def xml_document
  @xml_document
end

Class Method Details

.from_element(element) ⇒ Object



14
15
16
17
18
# File 'lib/sheng/merge_field.rb', line 14

def from_element(element)
  new(element)
rescue NotAMergeFieldError => e
  nil
end

Instance Method Details

#==(other) ⇒ Object



30
31
32
# File 'lib/sheng/merge_field.rb', line 30

def ==(other)
  other.is_a?(self.class) && other.element == element
end

#add_previous_sibling(fragment_to_add) ⇒ Object



148
149
150
151
152
153
154
155
156
# File 'lib/sheng/merge_field.rb', line 148

def add_previous_sibling(fragment_to_add)
  if inline?
    [xml].flatten.first.add_previous_sibling(fragment_to_add)
  elsif is_table_row_marker?
    containing_element.ancestors[1].add_previous_sibling(fragment_to_add)
  else
    containing_element.add_previous_sibling(fragment_to_add)
  end
end

#block_prefixObject



86
87
88
89
90
91
# File 'lib/sheng/merge_field.rb', line 86

def block_prefix
  @potential_prefix ||= begin
    potential_prefix = raw_key.match(REGEXES[:key_string])[:prefix]
    potential_prefix && potential_prefix.gsub(/\:$/, '')
  end
end

#block_typeObject



77
78
79
80
81
82
83
84
# File 'lib/sheng/merge_field.rb', line 77

def block_type
  return nil unless block_prefix
  if ["start", "end"].include?(block_prefix)
    Sequence
  else
    ConditionalBlock
  end
end

#comma_series_conjunctionObject



109
110
111
112
113
114
115
# File 'lib/sheng/merge_field.rb', line 109

def comma_series_conjunction
  if filters.detect { |f| f =~ /^series_with_commas\((.*)\)$/ }
    $1
  else
    "and"
  end
end

#containing_elementObject



131
132
133
134
# File 'lib/sheng/merge_field.rb', line 131

def containing_element
  parents_until_container = new_style? ? 2 : 1
  element.ancestors[parents_until_container - 1]
end

#filter_value(value) ⇒ Object



252
253
254
255
256
257
# File 'lib/sheng/merge_field.rb', line 252

def filter_value(value)
  filters.inject(value) { |val, filter_string|
    filterer = Filters.filter_for(filter_string)
    filterer.filter(val)
  }
end

#filtersObject



42
43
44
45
# File 'lib/sheng/merge_field.rb', line 42

def filters
  match = raw_key.match(REGEXES[:key_string])
  match[:filters].split("|").map(&:strip)
end

#get_value(data_set) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/sheng/merge_field.rb', line 227

def get_value(data_set)
  interpolated_string = key_parts.map { |token|
    if Support.is_numeric?(token) || MATH_TOKENS.include?(token)
      token
    else
      data_set.fetch(token)
    end
  }.join(" ")

  return interpolated_string unless key_has_math?

  Dentaku::Calculator.new.evaluate!(interpolated_string.gsub(",", ""))
end

#in_table_row?Boolean

Returns:

  • (Boolean)


140
141
142
# File 'lib/sheng/merge_field.rb', line 140

def in_table_row?
  containing_element.ancestors[1] && containing_element.ancestors[1].name == "tr"
end

#inline?Boolean

Returns:

  • (Boolean)


144
145
146
# File 'lib/sheng/merge_field.rb', line 144

def inline?
  containing_element.children.text != xml.text
end

#interpolate(data_set) ⇒ Object



241
242
243
244
245
246
247
248
249
250
# File 'lib/sheng/merge_field.rb', line 241

def interpolate(data_set)
  value = get_value(data_set)
  replace_mergefield(filter_value(value))
rescue DataSet::KeyNotFound, Dentaku::UnboundVariableError, Filters::UnsupportedFilterError => e
  @errors << e
  # Ignore this error; we'll collect all uninterpolated fields later and
  # raise a new exception, so we can list all the fields in an error
  # message.
  nil
end

#is_end?Boolean

Returns:

  • (Boolean)


117
118
119
# File 'lib/sheng/merge_field.rb', line 117

def is_end?
  block_prefix && block_prefix.match(/^end/)
end

#is_start?Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/sheng/merge_field.rb', line 93

def is_start?
  block_prefix && !block_prefix.match(/^end/)
end

#is_table_row_marker?Boolean

Returns:

  • (Boolean)


136
137
138
# File 'lib/sheng/merge_field.rb', line 136

def is_table_row_marker?
  in_table_row? && (is_start? || is_end?)
end

#iteration_variableObject



97
98
99
100
101
102
103
# File 'lib/sheng/merge_field.rb', line 97

def iteration_variable
  if filters.detect { |f| f =~ /^as\((.*)\)$/ }
    $1.to_sym
  else
    :item
  end
end

#keyObject



38
39
40
# File 'lib/sheng/merge_field.rb', line 38

def key
  raw_key.match(REGEXES[:key_string])[:key].strip
end

#key_has_math?Boolean

Returns:

  • (Boolean)


223
224
225
# File 'lib/sheng/merge_field.rb', line 223

def key_has_math?
  !(MATH_TOKENS & key_parts).empty?
end

#key_partsObject



195
196
197
198
199
200
201
202
203
204
# File 'lib/sheng/merge_field.rb', line 195

def key_parts
  @key_parts ||= key.gsub(",", "").
    gsub(".", "_DOTSEPARATOR_").
    split(/\b|\s/).
    map(&:strip).
    reject(&:empty?).
    map { |token|
      token.gsub("_DOTSEPARATOR_", ".")
    }
end

#new_style?Boolean

Returns:

  • (Boolean)


34
35
36
# File 'lib/sheng/merge_field.rb', line 34

def new_style?
  element.name == 'fldChar'
end

#next_elementObject



158
159
160
161
162
163
164
165
166
# File 'lib/sheng/merge_field.rb', line 158

def next_element
  if inline?
    [xml].flatten.last.next_element
  elsif is_table_row_marker?
    containing_element.ancestors[1].next_element
  else
    containing_element.next_element
  end
end

#raw_keyObject



47
48
49
# File 'lib/sheng/merge_field.rb', line 47

def raw_key
  @raw_key ||= @instruction_text.gsub(REGEXES[:instruction_text], '\1').strip
end

#removeObject



121
122
123
124
125
126
127
128
129
# File 'lib/sheng/merge_field.rb', line 121

def remove
  if inline?
    xml.remove
  elsif is_table_row_marker?
    containing_element.ancestors[1].remove
  else
    containing_element.remove
  end
end

#replace_mergefield(value) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/sheng/merge_field.rb', line 181

def replace_mergefield(value)
  value_as_string = if value.is_a?(BigDecimal)
    value.to_s("F")
  else
    value.to_s
  end

  new_run = Sheng::Support.new_text_run(
    value_as_string, xml_document: xml_document, style_run: styling_run
  )
  xml.before(new_run)
  xml.remove
end

#required_hash(placeholder: nil) ⇒ Object



212
213
214
215
216
217
218
219
220
221
# File 'lib/sheng/merge_field.rb', line 212

def required_hash(placeholder: nil)
  required_variables.inject({}) { |assembled, variable|
    parts = variable.split(/\./)
    last_key = parts.pop
    hash = parts.reverse.inject(last_key => placeholder) do |memo, key|
      memo = { key => memo }; memo
    end
    Sheng::Support.merge_required_hashes(assembled, hash)
  }
end

#required_variablesObject



206
207
208
209
210
# File 'lib/sheng/merge_field.rb', line 206

def required_variables
  key_parts.reject { |token|
    Support.is_numeric?(token) || MATH_TOKENS.include?(token)
  }
end

#series_with_commas?Boolean

Returns:

  • (Boolean)


105
106
107
# File 'lib/sheng/merge_field.rb', line 105

def series_with_commas?
  filters.detect { |f| f =~ /^series_with_commas/ }
end

#start_keyObject



67
68
69
70
71
72
73
74
75
# File 'lib/sheng/merge_field.rb', line 67

def start_key
  if is_start?
    "#{block_prefix}:#{key}"
  elsif block_prefix == "end"
    "start:#{key}"
  else
    "#{block_prefix.gsub(/^end_/, '')}:#{key}"
  end
end

#styling_paragraphObject



51
52
53
54
# File 'lib/sheng/merge_field.rb', line 51

def styling_paragraph
  return nil if inline?
  containing_element.at_xpath(".//w:pPr")
end

#styling_runObject



56
57
58
59
60
61
62
63
64
65
# File 'lib/sheng/merge_field.rb', line 56

def styling_run
  if new_style?
    separator_field = element.ancestors[1].at_xpath(".//w:fldChar[contains(@w:fldCharType, 'separate')]")
    if separator_field
      separator_field.parent.next_element.at_xpath(".//w:rPr")
    end
  else
    element.at_xpath(".//w:rPr")
  end
end

#xmlObject



168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/sheng/merge_field.rb', line 168

def xml
  return element unless new_style?
  nodeset = Nokogiri::XML::NodeSet.new(xml_document)
  current_node = element.parent
  nodeset << current_node
  loop do
    current_node = current_node.next_element
    nodeset << current_node
    break if current_node.at_xpath("./w:fldChar[contains(@w:fldCharType, 'end')]")
  end
  nodeset
end