plurimath 0.4.4 → 0.6.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 +4 -4
  2. data/.gitignore +4 -0
  3. data/.rspec +3 -0
  4. data/.rspec-opal +11 -0
  5. data/Gemfile +3 -0
  6. data/Rakefile +11 -0
  7. data/lib/plurimath/asciimath/transform.rb +15 -0
  8. data/lib/plurimath/latex/constants.rb +3 -0
  9. data/lib/plurimath/latex/parse.rb +20 -11
  10. data/lib/plurimath/latex/transform.rb +24 -2
  11. data/lib/plurimath/math/core.rb +88 -0
  12. data/lib/plurimath/math/formula.rb +68 -24
  13. data/lib/plurimath/math/function/base.rb +8 -2
  14. data/lib/plurimath/math/function/binary_function.rb +36 -4
  15. data/lib/plurimath/math/function/color.rb +14 -0
  16. data/lib/plurimath/math/function/fenced.rb +27 -0
  17. data/lib/plurimath/math/function/floor.rb +1 -1
  18. data/lib/plurimath/math/function/font_style.rb +46 -0
  19. data/lib/plurimath/math/function/frac.rb +6 -0
  20. data/lib/plurimath/math/function/int.rb +7 -0
  21. data/lib/plurimath/math/function/left.rb +19 -1
  22. data/lib/plurimath/math/function/lim.rb +6 -0
  23. data/lib/plurimath/math/function/limits.rb +7 -0
  24. data/lib/plurimath/math/function/log.rb +6 -0
  25. data/lib/plurimath/math/function/menclose.rb +6 -0
  26. data/lib/plurimath/math/function/mod.rb +6 -0
  27. data/lib/plurimath/math/function/msgroup.rb +28 -0
  28. data/lib/plurimath/math/function/multiscript.rb +7 -0
  29. data/lib/plurimath/math/function/nary.rb +94 -0
  30. data/lib/plurimath/math/function/oint.rb +6 -0
  31. data/lib/plurimath/math/function/over.rb +6 -0
  32. data/lib/plurimath/math/function/overset.rb +6 -0
  33. data/lib/plurimath/math/function/power.rb +8 -2
  34. data/lib/plurimath/math/function/power_base.rb +10 -31
  35. data/lib/plurimath/math/function/prod.rb +19 -18
  36. data/lib/plurimath/math/function/right.rb +19 -1
  37. data/lib/plurimath/math/function/root.rb +6 -0
  38. data/lib/plurimath/math/function/rule.rb +7 -0
  39. data/lib/plurimath/math/function/semantics.rb +6 -0
  40. data/lib/plurimath/math/function/stackrel.rb +6 -0
  41. data/lib/plurimath/math/function/substack.rb +6 -0
  42. data/lib/plurimath/math/function/sum.rb +26 -25
  43. data/lib/plurimath/math/function/table.rb +52 -24
  44. data/lib/plurimath/math/function/td.rb +28 -0
  45. data/lib/plurimath/math/function/ternary_function.rb +44 -4
  46. data/lib/plurimath/math/function/text.rb +25 -3
  47. data/lib/plurimath/math/function/tr.rb +28 -0
  48. data/lib/plurimath/math/function/unary_function.rb +43 -3
  49. data/lib/plurimath/math/function/underover.rb +7 -55
  50. data/lib/plurimath/math/function/underset.rb +6 -0
  51. data/lib/plurimath/math/function/vec.rb +40 -0
  52. data/lib/plurimath/math/function.rb +7 -5
  53. data/lib/plurimath/math/number.rb +9 -5
  54. data/lib/plurimath/math/symbol.rb +13 -9
  55. data/lib/plurimath/math.rb +1 -3
  56. data/lib/plurimath/mathml/parser.rb +4 -4
  57. data/lib/plurimath/mathml/transform.rb +3 -4
  58. data/lib/plurimath/omml/parser.rb +19 -3
  59. data/lib/plurimath/omml/transform.rb +19 -14
  60. data/lib/plurimath/setup/oga.rb +5 -0
  61. data/lib/plurimath/setup/opal.rb.erb +8 -0
  62. data/lib/plurimath/setup/ox.rb +5 -0
  63. data/lib/plurimath/utility.rb +60 -34
  64. data/lib/plurimath/version.rb +1 -1
  65. data/lib/plurimath/xml_engine/oga.rb +246 -0
  66. data/lib/plurimath/xml_engine/ox.rb +29 -0
  67. data/lib/plurimath/xml_engine.rb +6 -0
  68. data/lib/plurimath.rb +12 -2
  69. metadata +11 -2
@@ -19,9 +19,7 @@ require_relative "latex/parser"
19
19
  require_relative "html/parser"
20
20
  require_relative "omml/parser"
21
21
  require_relative "utility"
22
- require "ox"
23
22
  require "yaml"
24
- Ox.default_options = { encoding: "UTF-8" }
25
23
 
26
24
  module Plurimath
27
25
  module Math
@@ -72,7 +70,7 @@ module Plurimath
72
70
 
73
71
  def type_error!
74
72
  raise InvalidTypeError.new(
75
- "`type` must be one of: `#{VALID_TYPES.keys.join('`, `')}`"
73
+ "`type` must be one of: `#{VALID_TYPES.keys.join('`, `')}`",
76
74
  )
77
75
  end
78
76
 
@@ -23,8 +23,8 @@ module Plurimath
23
23
  end
24
24
 
25
25
  def parse
26
- ox_nodes = Ox.load(text, strip_namespace: true)
27
- display_style = ox_nodes&.locate("*/mstyle/@displaystyle")&.first
26
+ ox_nodes = Plurimath.xml_engine.load(text)
27
+ display_style = ox_nodes&.locate("mstyle/@displaystyle")&.first
28
28
  nodes = parse_nodes(ox_nodes.nodes)
29
29
  Math::Formula.new(
30
30
  Transform.new.apply(nodes).flatten.compact,
@@ -34,7 +34,7 @@ module Plurimath
34
34
 
35
35
  def parse_nodes(nodes)
36
36
  nodes.map do |node|
37
- next if node.is_a?(Ox::Comment)
37
+ next if Plurimath.xml_engine.is_xml_comment?(node)
38
38
 
39
39
  if node.is_a?(String)
40
40
  node
@@ -47,7 +47,7 @@ module Plurimath
47
47
  end
48
48
 
49
49
  def validate_attributes(attributes)
50
- attributes&.select! { |key, _| SUPPORTED_ATTRS.include?(key&.to_s) }
50
+ attributes&.select! { |key, _| SUPPORTED_ATTRS.include?(key.to_s) }
51
51
  attributes&.transform_keys(&:to_sym) if attributes&.any?
52
52
  end
53
53
 
@@ -88,8 +88,7 @@ module Plurimath
88
88
 
89
89
  rule(merror: sequence(:merror)) do
90
90
  Math::Function::Merror.new(
91
- merror[0],
92
- merror[1],
91
+ Utility.filter_values(merror),
93
92
  )
94
93
  end
95
94
 
@@ -247,7 +246,7 @@ module Plurimath
247
246
  symbols = Constants::UNICODE_SYMBOLS.transform_keys(&:to_s)
248
247
  text = entities.encode(mtext.flatten.join, :hexadecimal)
249
248
  symbols.each do |code, string|
250
- text.gsub!(code.downcase, "unicode[:#{string}]")
249
+ text = text.gsub(code.downcase, "unicode[:#{string}]")
251
250
  end
252
251
  Math::Function::Text.new(text)
253
252
  end
@@ -257,7 +256,7 @@ module Plurimath
257
256
  symbols = Constants::UNICODE_SYMBOLS.transform_keys(&:to_s)
258
257
  text = entities.encode(ms.first, :hexadecimal)
259
258
  symbols.each do |code, string|
260
- text.gsub!(code.downcase, "unicode[:#{string}]")
259
+ text = text.gsub(code.downcase, "unicode[:#{string}]")
261
260
  end
262
261
  Math::Function::Text.new(text)
263
262
  end
@@ -11,13 +11,29 @@ module Plurimath
11
11
  mr
12
12
  r
13
13
  ].freeze
14
+ SUPPORTED_FONTS = {
15
+ "sans-serif-bi": "sans-serif-bold-italic",
16
+ "double-struck": "double-struck",
17
+ "sans-serif-i": "sans-serif-italic",
18
+ "sans-serif-b": "bold-sans-serif",
19
+ "sans-serif-p": "sans-serif",
20
+ "fraktur-p": "fraktur",
21
+ "fraktur-b": "bold-fraktur",
22
+ "script-b": "bold-script",
23
+ "script-p": "script",
24
+ monospace: "monospace",
25
+ bi: "bold-italic",
26
+ p: "normal",
27
+ i: "italic",
28
+ b: "bold",
29
+ }.freeze
14
30
 
15
31
  def initialize(text)
16
32
  @text = text
17
33
  end
18
34
 
19
35
  def parse
20
- nodes = Ox.load(text, strip_namespace: true)
36
+ nodes = Plurimath.xml_engine.load(text)
21
37
  @hash = { sequence: parse_nodes(nodes.nodes) }
22
38
  nodes = JSON.parse(@hash.to_json, symbolize_names: true)
23
39
  Math::Formula.new(
@@ -55,14 +71,14 @@ module Plurimath
55
71
  end
56
72
 
57
73
  def organize_table_td(node)
58
- node.locate("e/?").each do |child_node|
74
+ node.locate("e/*").each do |child_node|
59
75
  child_node.name = "mtd" if child_node.name == "r"
60
76
  end
61
77
  end
62
78
 
63
79
  def organize_fonts(node)
64
80
  attrs_arr = { val: [] }
65
- node.locate("rPr/?").each do |child|
81
+ node.locate("rPr/*").each do |child|
66
82
  attrs_arr[:val] << child.attributes["val"]
67
83
  end
68
84
  node.attributes.merge! attrs_arr
@@ -72,7 +72,7 @@ module Plurimath
72
72
  font = flatten_row.shift
73
73
  font.new(
74
74
  Utility.filter_values(flatten_row),
75
- Utility::OMML_FONTS.invert[font].to_s,
75
+ Utility::FONT_STYLES.key(font).to_s,
76
76
  )
77
77
  else
78
78
  Utility.filter_values(flatten_row)
@@ -98,11 +98,17 @@ module Plurimath
98
98
  open_paren = fenced.shift if fenced&.first&.class_name == "symbol"
99
99
  close_paren = fenced.shift if fenced&.first&.class_name == "symbol"
100
100
  fenced_value = fenced.compact
101
- Math::Function::Fenced.new(
102
- open_paren,
103
- fenced_value,
104
- close_paren,
105
- )
101
+ if fenced_value.length == 1 && fenced_value.first.is_a?(Math::Function::Table)
102
+ fenced_value.first.open_paren = open_paren&.value
103
+ fenced_value.first.close_paren = close_paren&.value
104
+ fenced_value
105
+ else
106
+ Math::Function::Fenced.new(
107
+ open_paren,
108
+ fenced_value,
109
+ close_paren,
110
+ )
111
+ end
106
112
  end
107
113
 
108
114
  rule(dPr: subtree(:dpr)) do
@@ -116,7 +122,7 @@ module Plurimath
116
122
  font = flatten_mtd.shift
117
123
  font.new(
118
124
  Utility.filter_values(flatten_mtd),
119
- Utility::OMML_FONTS.invert[font].to_s,
125
+ Utility::FONT_STYLES.rassoc(font).first.to_s,
120
126
  )
121
127
  else
122
128
  flatten_mtd
@@ -133,7 +139,11 @@ module Plurimath
133
139
 
134
140
  rule(rPr: subtree(:rpr)) do
135
141
  if rpr.is_a?(Array)
136
- Utility::OMML_FONTS[rpr.join("-").to_sym]
142
+ Utility::FONT_STYLES[
143
+ Omml::Parser::SUPPORTED_FONTS[
144
+ rpr&.join("-")&.to_sym,
145
+ ]&.to_sym,
146
+ ]
137
147
  end
138
148
  end
139
149
 
@@ -172,12 +182,7 @@ module Plurimath
172
182
  ternary_class.parameter_three = Utility.filter_values(nary[3])
173
183
  ternary_class
174
184
  else
175
- Math::Formula.new(
176
- [
177
- Utility.nary_fonts(nary),
178
- Utility.filter_values(nary[3]),
179
- ],
180
- )
185
+ Utility.nary_fonts(nary)
181
186
  end
182
187
  end
183
188
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "plurimath/xml_engine/oga"
4
+
5
+ Plurimath.xml_engine = Plurimath::XMLEngine::Oga
@@ -0,0 +1,8 @@
1
+ require 'plurimath/math/core'
2
+ require 'plurimath/math/function'
3
+ <% (
4
+ Dir[File.dirname(__dir__)+"/math/function/*.rb"] +
5
+ Dir[File.dirname(__dir__)+"/math/function/**/*.rb"]
6
+ ).each do |f| %>
7
+ require 'plurimath/<%= f.split("lib/plurimath").last.gsub(/.rb$/,'') %>'
8
+ <% end %>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "plurimath/xml_engine/ox"
4
+
5
+ Plurimath.xml_engine = Plurimath::XMLEngine::Ox
@@ -4,24 +4,30 @@ module Plurimath
4
4
  class Utility
5
5
  FONT_STYLES = {
6
6
  "double-struck": Math::Function::FontStyle::DoubleStruck,
7
+ "sans-serif-bold-italic": Math::Function::FontStyle::SansSerifBoldItalic,
8
+ "sans-serif-italic": Math::Function::FontStyle::SansSerifItalic,
9
+ "bold-sans-serif": Math::Function::FontStyle::BoldSansSerif,
7
10
  "sans-serif": Math::Function::FontStyle::SansSerif,
11
+ "bold-fraktur": Math::Function::FontStyle::BoldFraktur,
12
+ "bold-italic": Math::Function::FontStyle::BoldItalic,
13
+ "bold-script": Math::Function::FontStyle::BoldScript,
8
14
  monospace: Math::Function::FontStyle::Monospace,
9
- fraktur: Math::Function::FontStyle::Fraktur,
10
- script: Math::Function::FontStyle::Script,
11
- normal: Math::Function::FontStyle::Normal,
12
- bold: Math::Function::FontStyle::Bold,
13
15
  mathfrak: Math::Function::FontStyle::Fraktur,
14
16
  mathcal: Math::Function::FontStyle::Script,
17
+ fraktur: Math::Function::FontStyle::Fraktur,
15
18
  mathbb: Math::Function::FontStyle::DoubleStruck,
16
19
  mathtt: Math::Function::FontStyle::Monospace,
17
20
  mathsf: Math::Function::FontStyle::SansSerif,
18
21
  mathrm: Math::Function::FontStyle::Normal,
19
22
  textrm: Math::Function::FontStyle::Normal,
23
+ italic: Math::Function::FontStyle::Italic,
20
24
  mathbf: Math::Function::FontStyle::Bold,
21
25
  textbf: Math::Function::FontStyle::Bold,
26
+ script: Math::Function::FontStyle::Script,
27
+ normal: Math::Function::FontStyle::Normal,
28
+ bold: Math::Function::FontStyle::Bold,
22
29
  bbb: Math::Function::FontStyle::DoubleStruck,
23
30
  cal: Math::Function::FontStyle::Script,
24
- bf: Math::Function::FontStyle::Bold,
25
31
  sf: Math::Function::FontStyle::SansSerif,
26
32
  tt: Math::Function::FontStyle::Monospace,
27
33
  fr: Math::Function::FontStyle::Fraktur,
@@ -29,6 +35,7 @@ module Plurimath
29
35
  cc: Math::Function::FontStyle::Script,
30
36
  ii: Math::Function::FontStyle::Italic,
31
37
  bb: Math::Function::FontStyle::Bold,
38
+ bf: Math::Function::FontStyle::Bold,
32
39
  }.freeze
33
40
  ALIGNMENT_LETTERS = {
34
41
  c: "center",
@@ -72,22 +79,6 @@ module Plurimath
72
79
  max
73
80
  min
74
81
  ].freeze
75
- OMML_FONTS = {
76
- "sans-serif-bi": Math::Function::FontStyle::SansSerifBoldItalic,
77
- "sans-serif-i": Math::Function::FontStyle::SansSerifItalic,
78
- "sans-serif-b": Math::Function::FontStyle::BoldSansSerif,
79
- "double-struck": Math::Function::FontStyle::DoubleStruck,
80
- "sans-serif-p": Math::Function::FontStyle::SansSerif,
81
- "fraktur-p": Math::Function::FontStyle::Fraktur,
82
- "fraktur-b": Math::Function::FontStyle::BoldFraktur,
83
- "script-b": Math::Function::FontStyle::BoldScript,
84
- "script-p": Math::Function::FontStyle::Script,
85
- monospace: Math::Function::FontStyle::Monospace,
86
- bi: Math::Function::FontStyle::BoldItalic,
87
- p: Math::Function::FontStyle::Normal,
88
- i: Math::Function::FontStyle::Italic,
89
- b: Math::Function::FontStyle::Bold,
90
- }.freeze
91
82
  PARENTHESIS = {
92
83
  "&#x2329;": "&#x232a;",
93
84
  "&#x230a;": "&#x230b;",
@@ -100,6 +91,12 @@ module Plurimath
100
91
  "{": "}",
101
92
  "[": "]",
102
93
  }.freeze
94
+ TEXT_CLASSES = %w[
95
+ unicode
96
+ symbol
97
+ number
98
+ text
99
+ ].freeze
103
100
 
104
101
  class << self
105
102
  def organize_table(array, column_align: nil, options: nil)
@@ -196,7 +193,7 @@ module Plurimath
196
193
  def ox_element(node, attributes: [], namespace: "")
197
194
  namespace = "#{namespace}:" unless namespace.empty?
198
195
 
199
- element = Ox::Element.new("#{namespace}#{node}")
196
+ element = Plurimath.xml_engine.new_element("#{namespace}#{node}")
200
197
  attributes&.each do |attr_key, attr_value|
201
198
  element[attr_key] = attr_value
202
199
  end
@@ -258,12 +255,14 @@ module Plurimath
258
255
 
259
256
  def nary_fonts(nary)
260
257
  narypr = nary.first.flatten.compact
261
- subsup = narypr.any?("undOvr") ? "underover" : "power_base"
258
+ subsup = narypr.any?("undOvr") ? "undOvr" : "subSup"
262
259
  unicode = narypr.any?(Hash) ? narypr.first[:chr] : "∫"
263
- get_class(subsup).new(
260
+ Math::Function::Nary.new(
264
261
  Math::Symbol.new(string_to_html_entity(unicode)),
265
- nary[1],
266
- nary[2],
262
+ filter_values(nary[1]),
263
+ filter_values(nary[2]),
264
+ filter_values(nary[3]),
265
+ { type: subsup }
267
266
  )
268
267
  end
269
268
 
@@ -292,7 +291,8 @@ module Plurimath
292
291
  end
293
292
 
294
293
  def td_value(td_object)
295
- if td_object.is_a?(String) && td_object.empty?
294
+ str_classes = [String, Parslet::Slice]
295
+ if str_classes.include?(td_object.class) && td_object.to_s.empty?
296
296
  return Math::Function::Text.new(nil)
297
297
  end
298
298
 
@@ -461,7 +461,7 @@ module Plurimath
461
461
  end
462
462
 
463
463
  def left_right_objects(paren, function)
464
- paren = if paren.to_s.match?(/\\{|\\}/)
464
+ paren = if paren.to_s.match?(/\\\{|\\\}/)
465
465
  paren.to_s.gsub(/\\/, "")
466
466
  else
467
467
  Latex::Constants::LEFT_RIGHT_PARENTHESIS[paren.to_sym]
@@ -478,12 +478,7 @@ module Plurimath
478
478
  def mrow_left_right(mrow = [])
479
479
  object = mrow.first
480
480
  !(
481
- (
482
- (
483
- object.is_a?(Math::Function::TernaryFunction) && object.any_value_exist?
484
- ) &&
485
- (mrow.length <= 2)
486
- ) ||
481
+ ((object.is_a?(Math::Function::TernaryFunction) && object.any_value_exist?) && (mrow.length <= 2)) ||
487
482
  (object.is_a?(Math::Function::UnaryFunction) && mrow.length == 1)
488
483
  )
489
484
  end
@@ -573,6 +568,37 @@ module Plurimath
573
568
  end
574
569
  end
575
570
  end
571
+
572
+ def validate_math_zone(object)
573
+ return false unless object
574
+
575
+ if object.is_a?(Math::Formula)
576
+ filter_math_zone_values(object.value).find do |d|
577
+ !d.is_a?(Math::Function::Text)
578
+ end
579
+ else
580
+ !TEXT_CLASSES.include?(object.class_name)
581
+ end
582
+ end
583
+
584
+ def filter_math_zone_values(value)
585
+ return [] if value&.empty?
586
+
587
+ new_arr = []
588
+ temp_array = []
589
+ skip_index = nil
590
+ value.each_with_index do |obj, index|
591
+ object = obj.dup
592
+ next if index == skip_index
593
+ next temp_array << object.value if TEXT_CLASSES.include?(object.class_name)
594
+
595
+ new_arr << Math::Function::Text.new(temp_array.join(" ")) if temp_array.any?
596
+ temp_array = []
597
+ new_arr << object
598
+ end
599
+ new_arr << Math::Function::Text.new(temp_array.join(" ")) if temp_array.any?
600
+ new_arr
601
+ end
576
602
  end
577
603
  end
578
604
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plurimath
4
- VERSION = "0.4.4"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "plurimath/xml_engine"
4
+ require "corelib/array/pack" if RUBY_ENGINE == "opal"
5
+ require "oga"
6
+
7
+ module Plurimath
8
+ module XMLEngine
9
+ class Oga
10
+ class << self
11
+ def new_element(name)
12
+ data = ::Oga::XML::Element.new(name: name)
13
+ Node.new(data)
14
+ end
15
+
16
+ def dump(data, indent: nil)
17
+ Dumper.new(data, indent: indent).dump.out
18
+ end
19
+
20
+ def load(data)
21
+ data = ::Oga::XML::Parser.new(data, html: true).parse
22
+ if data.xml_declaration
23
+ Document.new(data)
24
+ else
25
+ Document.new(data).nodes.first
26
+ end
27
+ end
28
+
29
+ def is_xml_comment?(node)
30
+ node = node.unwrap if node.respond_to? :unwrap
31
+ node.is_a?(Comment)
32
+ end
33
+ end
34
+
35
+ # Create API compatible with Ox, per Plurimath usage
36
+ class Wrapper
37
+ def initialize(value)
38
+ @wrapped = value
39
+ end
40
+
41
+ def unwrap
42
+ @wrapped
43
+ end
44
+
45
+ def ==(other)
46
+ self.class == other.class &&
47
+ @wrapped.inspect == other.unwrap.inspect
48
+ end
49
+ end
50
+
51
+ class Node < Wrapper
52
+ # Ox removes text nodes that are whitespace-only.
53
+ # There exists a weird edge case on which Plurimath depends:
54
+ # <mi> <!-- xxx --> &#x3C0;<!--GREEK SMALL LETTER PI--> </mi>
55
+ # If the last text node of an element that does not contain other
56
+ # elements is a whitespace, it preserves it. The first one can be
57
+ # safely removed.
58
+ def nodes
59
+ children = @wrapped.children
60
+ length = children.length
61
+ preserve_last = true
62
+ children.map.with_index do |i,idx|
63
+ if preserve_last && idx == length-1 && i.is_a?(::Oga::XML::Text)
64
+ i.text
65
+ elsif i.is_a? ::Oga::XML::Text
66
+ remove_indentation(i)
67
+ elsif i.is_a? ::Oga::XML::Comment
68
+ Node.new(i)
69
+ else
70
+ preserve_last = false
71
+ Node.new(i)
72
+ end
73
+ end.compact
74
+ end
75
+
76
+ def [](attr)
77
+ attr = attr.to_s
78
+
79
+ @wrapped.attributes.each do |e|
80
+ return e.value if [e.name, e.name.split(":").last].include? attr
81
+ end
82
+
83
+ nil
84
+ end
85
+
86
+ def []=(attr, value)
87
+ # Here we tap into the internal representation due to some likely
88
+ # bug in Oga
89
+ attr = ::Oga::XML::Attribute.new(name: attr.to_s)
90
+ attr.element = @wrapped
91
+ attr.instance_variable_set(:@value, value.to_s)
92
+ attr.instance_variable_set(:@decoded, true)
93
+ @wrapped.attributes << attr
94
+ end
95
+
96
+ def <<(other)
97
+ other = other.unwrap if other.respond_to? :unwrap
98
+
99
+ case other
100
+ when String
101
+ text = other
102
+ # Here we tap into the internal representation due to some likely
103
+ # bug in Oga
104
+ other = ::Oga::XML::Text.new
105
+ other.instance_variable_set(:@from_plurimath, true)
106
+ other.instance_variable_set(:@text, text)
107
+ other.instance_variable_set(:@decoded, true)
108
+ end
109
+
110
+ @wrapped.children << other.dup
111
+ self
112
+ end
113
+
114
+ def attributes
115
+ @wrapped.attributes.to_h do |e|
116
+ [e.name.split(":").last, e.value]
117
+ end
118
+ end
119
+
120
+ def locate(xpath)
121
+ @wrapped.xpath(xpath).map do |i|
122
+ case i
123
+ when ::Oga::XML::Text
124
+ i.text
125
+ when ::Oga::XML::Attribute
126
+ i.value
127
+ else
128
+ Node.new(i)
129
+ end
130
+ end
131
+ end
132
+
133
+ def name
134
+ @wrapped.name
135
+ end
136
+
137
+ def name=(new_name)
138
+ @wrapped.name = new_name
139
+ end
140
+
141
+ private
142
+
143
+ def remove_indentation(text)
144
+ from_us = text.instance_variable_get(:@from_plurimath)
145
+ !from_us && text.text.strip == "" ? nil : text.text
146
+ end
147
+ end
148
+
149
+ class Document < Node
150
+ end
151
+
152
+ Comment = ::Oga::XML::Comment
153
+
154
+ # Dump the tree just as if we were Ox. This is a limited implementation.
155
+ class Dumper
156
+ def initialize(tree, indent: nil)
157
+ @tree = tree
158
+ @indent = indent
159
+ @depth = 0
160
+ @out = ""
161
+ end
162
+
163
+ def dump(node = @tree)
164
+ case node
165
+ when Node
166
+ nodes = node.nodes
167
+ if nodes.length == 0
168
+ line_break
169
+ @out += "<#{node.unwrap.name}#{dump_attrs(node)}/>"
170
+ else
171
+ line_break
172
+ @out += "<#{node.unwrap.name}#{dump_attrs(node)}>"
173
+ @depth += 1
174
+ nodes.each { |i| dump(i) }
175
+ @depth -= 1
176
+ line_break unless nodes.last.is_a?(::String)
177
+ @out += "</#{node.unwrap.name}>"
178
+ end
179
+ when ::String
180
+ @out += entities(node)
181
+ end
182
+
183
+ line_break if node.object_id == @tree.object_id
184
+
185
+ self
186
+ end
187
+
188
+ attr_reader :out
189
+
190
+ ORD_AMP="&".ord
191
+ ORD_LT="<".ord
192
+ ORD_GT=">".ord
193
+ ORD_APOS="'".ord
194
+ ORD_QUOT='"'.ord
195
+ ORD_NEWLINE="\n".ord
196
+ ORD_CARRIAGERETURN="\r".ord
197
+
198
+ def self.entities(text,attr=false)
199
+ text.to_s.chars.map(&:ord).map do |i|
200
+ if i == ORD_AMP
201
+ "&amp;"
202
+ elsif i == ORD_LT
203
+ "&lt;"
204
+ elsif i == ORD_GT
205
+ "&gt;"
206
+ elsif i == ORD_QUOT && attr
207
+ "&quot;"
208
+ elsif i == ORD_NEWLINE || i == ORD_CARRIAGERETURN
209
+ i.chr("utf-8")
210
+ elsif i < 0x20
211
+ "&#x#{i.to_s(16).rjust(4, "0")};"
212
+ else
213
+ i.chr("utf-8")
214
+ end
215
+ end.join
216
+ end
217
+
218
+ private
219
+
220
+ def dump_attrs(node)
221
+ node.unwrap.attributes.map do |i|
222
+ # Currently, this is not part of the contract. But in the future
223
+ # it may be needed to also handle namespaces:
224
+ #
225
+ # if i.namespace
226
+ # %{ #{i.namespace.name}:#{i.name}="#{attr_entities i.value}"}
227
+ %{ #{i.name}="#{attr_entities i.value}"}
228
+ end.join
229
+ end
230
+
231
+ def entities(text)
232
+ self.class.entities(text)
233
+ end
234
+
235
+ def attr_entities(text)
236
+ self.class.entities(text, true)
237
+ end
238
+
239
+ def line_break
240
+ @out += "\n"
241
+ @out += " " * (@indent * @depth) if @indent
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "plurimath/xml_engine"
4
+ require "ox"
5
+ Ox.default_options = { encoding: "UTF-8" }
6
+
7
+ module Plurimath
8
+ module XMLEngine
9
+ class Ox
10
+ class << self
11
+ def new_element(name)
12
+ ::Ox::Element.new(name)
13
+ end
14
+
15
+ def dump(data, **options)
16
+ ::Ox.dump(data, **options)
17
+ end
18
+
19
+ def load(data)
20
+ ::Ox.load(data, strip_namespace: true)
21
+ end
22
+
23
+ def is_xml_comment?(node)
24
+ node.is_a?(::Ox::Comment)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module Plurimath
2
+ singleton_class.attr_accessor :xml_engine
3
+
4
+ module XMLEngine
5
+ end
6
+ end