asciichem 0.29.2 → 0.30.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f372a8539906c658a399b015255e574de5feb73d1b5e7183958901681bfbce34
4
- data.tar.gz: 7269dc4ad6e39e7c5ea6726b54a8ffe1508c039bdf9ced5c0a74e23efdc9b00f
3
+ metadata.gz: cf3a968f977add504a84afdbeb3d5d231b959688f4d9c9ac70e424a251ea2f75
4
+ data.tar.gz: 8b8c12366ce9f05f44058b897b474f226dea561568ee6c3e17820dda4c5bd4f7
5
5
  SHA512:
6
- metadata.gz: 6f936bc0e95907d589c0095d34e4e65c554dcbdccd276aab7186db8f1218cbb179652b082f088f1b70a76391e830db53de2259ea8d333b64dd85ba367b16c610
7
- data.tar.gz: 63d979830e4a8ab9aca450006a9740e83428e26073323af0ed4f8ddeed4be6f7aecb8bcfe1e5173f7a6049f92ff8b0a8e0a2b374fbd13630ae00b0e9e7a0db60
6
+ metadata.gz: 6ae161dc4a0eef5ec494ee42a5c7e1280cddc02c0cfb3fa5701377bc6fcf82ddc32a37039b24ccaa2666959f39179d0fa7f7a48d0c91fca14829679bdbdc5e03
7
+ data.tar.gz: 47124baeb7589216a48176c718a7556536334203f1d01f941de501a186750cfe00dc7151589295790a16fb37efa5f688709eef28b9d88ad7f7da9e6191140207
data/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
3
3
  All notable changes to AsciiChem are documented here.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.30.0] - 2026-09-22
7
+
8
+ ### Changed
9
+ - The XML layer migrates from Nokogiri to
10
+ [moxml](https://github.com/lutaml/moxml) — the adapter layer the
11
+ rest of the lutaml ecosystem (chemicalml, CML wire, metanorma
12
+ gems) already uses, so AsciiChem follows the consumer's
13
+ configured XML backend instead of pinning one. The gemspec
14
+ dependency changes accordingly (`nokogiri ~> 1.18` out, `moxml
15
+ ~> 0.5` in). MathML output is byte-identical to 0.29.3: the L2
16
+ corpus goldens pass unchanged, including empty-element
17
+ compaction and the `<?xml?>` declaration.
18
+
19
+ ## [0.29.3] - 2026-09-22
20
+
21
+ ### Changed
22
+ - Dependency floors raised to the corpus-validated versions,
23
+ expressed pessimistically (`~>`): `lutaml-model ~> 0.8` (from
24
+ `>= 0.8, < 2`), `relaton-bib ~> 2.1` (from `>= 0.1, < 3`),
25
+ `plurimath ~> 0.11`, `nokogiri ~> 1.18`; `chemicalml`, `elkrb`,
26
+ `mml`, `parslet`, `thor` unchanged. Full suite green under the
27
+ raised floors (1985 examples).
28
+
6
29
  ## [0.29.2] - 2026-09-17
7
30
 
8
31
  ### Changed
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ source "https://rubygems.org"
5
5
  gemspec
6
6
 
7
7
  group :development do
8
+ gem "parsanol"
8
9
  gem "json_schemer", "~> 2.4"
9
10
  gem "benchmark", "~> 0.4"
10
11
  gem "benchmark-ips", "~> 2.14", require: false
data/asciichem.gemspec CHANGED
@@ -33,14 +33,16 @@ Gem::Specification.new do |spec|
33
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
34
  spec.require_paths = ["lib"]
35
35
 
36
+ # Floors are the versions the corpus suite validates against
37
+ # (pessimistic ~>); raise them only with a full-suite run.
36
38
  spec.add_dependency "chemicalml", "~> 0.3.0"
37
39
  spec.add_dependency "elkrb", "~> 1.0"
38
- spec.add_dependency "lutaml-model", ">= 0.8", "< 2"
40
+ spec.add_dependency "lutaml-model", "~> 0.8"
39
41
  spec.add_dependency "mml", "~> 2.3"
40
- spec.add_dependency "nokogiri", "~> 1.16"
42
+ spec.add_dependency "moxml", "~> 0.5"
41
43
  spec.add_dependency "parslet", "~> 2.0"
42
- spec.add_dependency "relaton-bib", ">= 0.1", "< 3"
43
- spec.add_dependency "plurimath", "~> 0.8"
44
+ spec.add_dependency "relaton-bib", "~> 2.1"
45
+ spec.add_dependency "plurimath", "~> 0.11"
44
46
  spec.add_dependency "thor", "~> 1.3"
45
47
 
46
48
  spec.add_development_dependency "json_schemer", "~> 2.4"
data/benchmarks/README.md CHANGED
@@ -168,3 +168,65 @@ non-blocking (#36 bare repeated sibling captures remains open but
168
168
  is worked around in `ParsanolEngine` via single `.as(...)` capture
169
169
  wrapping).
170
170
 
171
+
172
+ ### Re-check 8 (2026-09-17, parsanol 1.3.27)
173
+
174
+ All four upstream issues we filed are now closed (#36-#39; seven
175
+ releases since 1.3.20). Validation:
176
+
177
+ - Gate **221/221** through the shipped `ParsanolEngine`.
178
+ - **#36 verified fixed at the source**: the bare-repeated-sibling
179
+ repro (`A->B->C`) now returns parslet's array-of-segment-hashes —
180
+ every match preserved. Our `split_merged_formula` seam in
181
+ `ParsanolEngine` is therefore a compatibility no-op on current
182
+ parsanol (it still normalizes the merged-hash shape for older
183
+ parsanol lines, which the opt-in floor allows).
184
+ - **Perf: ratio-only this time.** The machine ran at load ~45
185
+ during measurement (parallel spec suites in other sessions), so
186
+ absolute numbers are meaningless — the parslet control itself
187
+ measured 15-20x slower than its quiet-machine baseline.
188
+ Same-process ratio: parsanol **~2.1x parslet** (8.3 vs 3.8 i/s,
189
+ and 8.8 vs 4.5 on the repeat), consistent with the 2.6x
190
+ quiet-machine figure from re-check 7.
191
+
192
+ ### Re-check 9 (2026-09-22, parsanol 1.3.49)
193
+
194
+ Twenty-one releases since re-check 8, all perf-focused upstream
195
+ (#59 roadmap: first-set BYTE_DISPATCH 1.3.29, VM memoisation
196
+ 1.3.35, VM phase-2 wiring 1.3.33, native dynamic bridge fixes
197
+ 1.3.40-1.3.41, `Parsanol::IncrementalSession` 1.3.42). Validation:
198
+
199
+ - Gate **221/221** through the shipped `ParsanolEngine` (run on
200
+ 1.3.48/1.3.49 within the same day — the line is moving fast).
201
+ - **Perf: ratio-only again.** Load was 34-77 during measurement
202
+ (parslet control itself ran 8-12 i/s vs its quiet ~75), so
203
+ absolute numbers are excluded. Same-process ratio across three
204
+ runs: parsanol **1.7-3.2x parslet** (37.7/11.7, 19.0/11.1,
205
+ 25.5/8.4), centring ~2.5-3x — consistent with the quiet-machine
206
+ 2.6x from re-check 7; under contention the native parse path
207
+ degrades less than pure-Ruby parslet.
208
+ - Upstream's incremental (`Parsanol::IncrementalSession`) and VM
209
+ memoisation work benefits the compat layer automatically; no
210
+ asciichem-side change needed or made.
211
+
212
+ ## Leptris note (2026-09-22, 1.9.221)
213
+
214
+ Leptris is moxml's PREFERRED_ADAPTER: when installed above its
215
+ binding floor, lutaml-model's XML layer (our CML wire path) runs on
216
+ it. The version is fully transitive — lutaml-model constrains
217
+ `~> 1.9.178`; asciichem pins nothing. The line moves fast
218
+ (1.9.178 floor -> 1.9.222 within days).
219
+
220
+ - **Compatibility:** full suite **1985/0** at 1.9.221.1, including
221
+ every CML round-trip and three-way wire spec. 1.9.222's namespace
222
+ fix (`xml:space` in the interleaved lane, reported upstream by
223
+ Canon) does not affect our documents; no action.
224
+ - **Perf on our CML workload:** leptris is ~15-25% behind nokogiri
225
+ (round-trip 12.7 vs 15.0 i/s; emit 30.6 vs 39.8 i/s; load-noisy
226
+ ±20%). The workload is dominated by lutaml-model's Ruby-side
227
+ model building, not the adapter — leptris's speed gains target
228
+ its native parse lanes (HTML/XQuery per its release notes).
229
+ Measurement caveat: forcing an adapter for A/B runs requires
230
+ stubbing `leptris_preferred_available?` — lutaml's
231
+ `detect_xml_adapter` calls `runtime_default_adapter`, which
232
+ ignores `default_adapter=`.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogiri'
3
+ require 'moxml'
4
4
 
5
5
  module AsciiChem
6
6
  module Cml
@@ -26,11 +26,11 @@ module AsciiChem
26
26
  return xml if conditions_map.empty?
27
27
 
28
28
  # If native <conditionList> already present, skip aci: emit.
29
- doc_check = Nokogiri::XML(xml)
29
+ doc_check = Moxml.parse(xml)
30
30
  return xml if doc_check.xpath('//cml:reaction/cml:conditionList',
31
31
  cml: Extensions::CML_NS).any?
32
32
 
33
- doc = Nokogiri::XML(xml)
33
+ doc = Moxml.parse(xml)
34
34
  root = doc.root
35
35
  Extensions.ensure_namespace(root)
36
36
  apply_conditions(root, conditions_map)
@@ -40,7 +40,7 @@ module AsciiChem
40
40
  # Extract aci:conditionsAbove/Below from each <reaction>.
41
41
  # Returns `{ reaction_id => { above:, below: } }`.
42
42
  def self.extract(xml)
43
- doc = Nokogiri::XML(xml)
43
+ doc = Moxml.parse(xml)
44
44
  result = {}
45
45
  doc.xpath('//cml:reaction', cml: Extensions::CML_NS).each do |el|
46
46
  id = el['id']
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogiri'
3
+ require 'moxml'
4
4
 
5
5
  module AsciiChem
6
6
  module Cml
@@ -39,9 +39,9 @@ module AsciiChem
39
39
  def self.inject(xml, extensions)
40
40
  return xml if extensions.empty?
41
41
 
42
- doc = Nokogiri::XML(xml)
42
+ doc = Moxml.parse(xml)
43
43
  root = doc.root
44
- unless root.namespaces.value?(Extensions::NAMESPACE)
44
+ unless root.namespaces.any? { |ns| ns.uri == Extensions::NAMESPACE }
45
45
  root.add_namespace(Extensions::PREFIX, Extensions::NAMESPACE)
46
46
  end
47
47
 
@@ -62,7 +62,7 @@ module AsciiChem
62
62
  # Extract aci: attributes from a CML XML string. Returns a map
63
63
  # `{ atom_id => { field: value } }` with Ruby-native types.
64
64
  def self.extract(xml)
65
- doc = Nokogiri::XML(xml)
65
+ doc = Moxml.parse(xml)
66
66
  result = {}
67
67
  doc.xpath('//cml:atom', cml: Extensions::CML_NS).each do |atom_el|
68
68
  atom_id = atom_el['id']
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogiri'
3
+ require 'moxml'
4
4
  require 'set'
5
5
 
6
6
  module AsciiChem
@@ -120,9 +120,9 @@ module AsciiChem
120
120
  def self.inject(xml, top_level)
121
121
  return xml if top_level.empty?
122
122
 
123
- doc = Nokogiri::XML(xml)
123
+ doc = Moxml.parse(xml)
124
124
  root = doc.root
125
- unless root.namespaces.value?(Extensions::NAMESPACE)
125
+ unless root.namespaces.any? { |ns| ns.uri == Extensions::NAMESPACE }
126
126
  root.add_namespace(Extensions::PREFIX, Extensions::NAMESPACE)
127
127
  end
128
128
  top_level.each { |entry| insert_element(doc, root, entry) }
@@ -133,7 +133,7 @@ module AsciiChem
133
133
  # array of `{ position:, element_name:, content: }` hashes in
134
134
  # ascending position order.
135
135
  def self.extract(xml)
136
- doc = Nokogiri::XML(xml)
136
+ doc = Moxml.parse(xml)
137
137
  result = []
138
138
  element_names = HANDLERS.map(&:element_name)
139
139
  element_names.each do |name|
@@ -174,7 +174,7 @@ module AsciiChem
174
174
  def insert_element(doc, root, entry)
175
175
  element = doc.create_element("#{Extensions::PREFIX}:#{entry[:element_name]}")
176
176
  element['position'] = entry[:position].to_s
177
- element.content = entry[:content]
177
+ element.add_child(doc.create_text(entry[:content]))
178
178
  # Insert before existing children so extensions appear at
179
179
  # the top of <cml>, which reads more naturally than appended.
180
180
  root.children.first&.add_previous_sibling(element) || root.add_child(element)
@@ -31,7 +31,7 @@ module AsciiChem
31
31
  # extension module that injects aci: attributes or elements
32
32
  # calls this instead of inlining its own check.
33
33
  def self.ensure_namespace(root)
34
- return if root.namespaces.value?(NAMESPACE)
34
+ return if root.namespaces.any? { |ns| ns.uri == NAMESPACE }
35
35
 
36
36
  root.add_namespace(PREFIX, NAMESPACE)
37
37
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogiri'
3
+ require 'moxml'
4
4
 
5
5
  module AsciiChem
6
6
  module Cml
@@ -46,7 +46,7 @@ module AsciiChem
46
46
  def self.inject(xml, groups_by_molecule)
47
47
  return xml if groups_by_molecule.empty?
48
48
 
49
- doc = Nokogiri::XML(xml)
49
+ doc = Moxml.parse(xml)
50
50
  root = doc.root
51
51
  Extensions.ensure_namespace(root)
52
52
 
@@ -77,7 +77,7 @@ module AsciiChem
77
77
  # molecule's ID. Each value is an array of record hashes:
78
78
  # `{ multiplicity:, bracket:, atom_ids: }`.
79
79
  def self.extract(xml)
80
- doc = Nokogiri::XML(xml)
80
+ doc = Moxml.parse(xml)
81
81
  result = Hash.new { |h, k| h[k] = [] }
82
82
  doc.xpath('//cml:molecule', cml: Extensions::CML_NS).each do |mol_el|
83
83
  group_els = mol_el.xpath("./#{Extensions::PREFIX}:group",
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogiri'
3
+ require 'moxml'
4
4
 
5
5
  module AsciiChem
6
6
  module Cml
@@ -28,7 +28,7 @@ module AsciiChem
28
28
  metadata_map = build_metadata_map(formula)
29
29
  return xml if metadata_map.empty?
30
30
 
31
- doc = Nokogiri::XML(xml)
31
+ doc = Moxml.parse(xml)
32
32
  root = doc.root
33
33
  Extensions.ensure_namespace(root)
34
34
  apply_metadata(root, metadata_map)
@@ -38,7 +38,7 @@ module AsciiChem
38
38
  # Extract aci:meta-* attributes from each <molecule> in the XML.
39
39
  # Returns a map `{ molecule_id => { name => content } }`.
40
40
  def self.extract(xml)
41
- doc = Nokogiri::XML(xml)
41
+ doc = Moxml.parse(xml)
42
42
  result = {}
43
43
  doc.xpath('//cml:molecule', cml: Extensions::CML_NS).each do |el|
44
44
  id = el['id']
@@ -132,11 +132,11 @@ module AsciiChem
132
132
  end
133
133
 
134
134
  def read_meta_attrs(element)
135
- element.attributes.each_with_object({}) do |(name, attr), memo|
136
- next unless name.start_with?(META_PREFIX)
135
+ element.attributes.each_with_object({}) do |attr, memo|
136
+ next unless attr.name.start_with?(META_PREFIX)
137
137
  next unless attr.namespace&.prefix == Extensions::PREFIX
138
138
 
139
- memo[name.sub(META_PREFIX, '')] = attr.value
139
+ memo[attr.name.sub(META_PREFIX, '')] = attr.value
140
140
  end
141
141
  end
142
142
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "nokogiri"
3
+ require "moxml"
4
4
 
5
5
  module AsciiChem
6
6
  module Cml
@@ -42,9 +42,9 @@ module AsciiChem
42
42
  def self.inject(xml, formula)
43
43
  return xml if formula.nodes.empty?
44
44
 
45
- doc = Nokogiri::XML(xml)
45
+ doc = Moxml.parse(xml)
46
46
  root = doc.root
47
- wire_children = root.element_children
47
+ wire_children = root.children.select(&:element?)
48
48
  inserts = build_inserts(formula, wire_children.length)
49
49
  return xml if inserts.empty?
50
50
 
@@ -54,11 +54,11 @@ module AsciiChem
54
54
 
55
55
  def self.apply_inserts(root, wire_children, inserts)
56
56
  inserts.reverse_each do |wire_index, raw_xml|
57
- fragment = Nokogiri::XML::DocumentFragment.parse(raw_xml)
57
+ nodes = Moxml.new.parse_fragment(raw_xml)
58
58
  if wire_index >= wire_children.length
59
- root.add_child(fragment)
59
+ nodes.each { |n| root.add_child(n) }
60
60
  else
61
- wire_children[wire_index].add_previous_sibling(fragment)
61
+ nodes.each { |n| wire_children[wire_index].add_previous_sibling(n) }
62
62
  end
63
63
  end
64
64
  end
@@ -88,10 +88,10 @@ module AsciiChem
88
88
  # document order. Also returns the cleaned XML with the unknown
89
89
  # elements removed (so chemicalml's parser doesn't trip).
90
90
  def self.extract(xml)
91
- doc = Nokogiri::XML(xml)
91
+ doc = Moxml.parse(xml)
92
92
  root = doc.root
93
93
  result = []
94
- children = root.element_children
94
+ children = root.children.select(&:element?)
95
95
  children.each_with_index do |child, idx|
96
96
  next if cml_namespace?(child)
97
97
  next if aci_namespace?(child)
@@ -107,14 +107,12 @@ module AsciiChem
107
107
  end
108
108
 
109
109
  def self.cml_namespace?(element)
110
- ns = element.namespace
111
- ns && ns.href == Extensions::CML_NS
110
+ element.namespace_uri == Extensions::CML_NS
112
111
  end
113
112
  private_class_method :cml_namespace?
114
113
 
115
114
  def self.aci_namespace?(element)
116
- ns = element.namespace
117
- ns && ns.href == Extensions::NAMESPACE
115
+ element.namespace_uri == Extensions::NAMESPACE
118
116
  end
119
117
  private_class_method :aci_namespace?
120
118
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "nokogiri"
3
+ require "moxml"
4
4
  require "mml"
5
5
 
6
6
  module AsciiChem
@@ -16,19 +16,26 @@ module AsciiChem
16
16
  MATHML_NS = "http://www.w3.org/1998/Math/MathML".freeze
17
17
 
18
18
  def initialize
19
- @doc = Nokogiri::XML::Document.new
19
+ @doc = Moxml.new.create_document
20
20
  end
21
21
 
22
22
  # Model entry point. Returns a `<math>` element as a string.
23
23
  def visit_formula(formula)
24
- math = el("math", xmlns: MATHML_NS)
24
+ math = @doc.create_element("math")
25
+ math.add_namespace(nil, MATHML_NS)
25
26
  mrow = el("mrow")
26
27
  formula.nodes.each { |n| mrow.add_child(render_node(n)) }
27
28
  math.add_child(mrow)
29
+ @doc.add_child(@doc.create_declaration("1.0", "UTF-8", nil))
28
30
  @doc.root = math
29
31
  # Native UTF-8 output preserves unicode arrow entities so specs
30
32
  # and downstream consumers see ⇌ and → instead of &#x21CC;.
31
- @doc.to_xml(encoding: "UTF-8")
33
+ # Empty MathML elements are compacted back to <none/> form: the
34
+ # L2 corpus goldens are byte-exact across the TypeScript and
35
+ # Python implementations, whose serializers self-close.
36
+ @doc.to_xml(encoding: "UTF-8", indent: 2, indent_text: " ")
37
+ .gsub("<none></none>", "<none/>")
38
+ .gsub("<mprescripts></mprescripts>", "<mprescripts/>")
32
39
  end
33
40
 
34
41
  def visit_molecule(molecule)
@@ -72,7 +79,7 @@ module AsciiChem
72
79
  multi.add_child(base)
73
80
  multi.add_child(el("none"))
74
81
  multi.add_child(el("none"))
75
- multi.add_child(Nokogiri::XML::Element.new("mprescripts", @doc))
82
+ multi.add_child(@doc.create_element("mprescripts"))
76
83
  multi.add_child(el("none"))
77
84
  multi.add_child(mn(atom.isotope))
78
85
  multi
@@ -211,12 +218,13 @@ module AsciiChem
211
218
  next unless attr_name
212
219
 
213
220
  Array(math.public_send(attr_name)).each do |child|
214
- fragment = Nokogiri::XML::DocumentFragment.parse(child.to_xml)
215
- fragment.children.each { |node| mrow.add_child(node) }
221
+ fragment = Moxml.new.parse_fragment(child.to_xml)
222
+ # parse_fragment returns the fragment's top-level nodes
223
+ fragment.each { |node| mrow.add_child(node) }
216
224
  end
217
225
  end
218
226
  mrow
219
- rescue Mml::Error, Lutaml::Model::Error, Nokogiri::XML::SyntaxError
227
+ rescue Mml::Error, Lutaml::Model::Error, Moxml::ParseError
220
228
  el("mrow")
221
229
  end
222
230
 
@@ -388,30 +396,30 @@ module AsciiChem
388
396
  msup
389
397
  end
390
398
 
391
- # -- Nokogiri element factories ------------------------------------
399
+ # -- Element factories ------------------------------------
392
400
 
393
401
  def el(name, attrs = {})
394
- element = Nokogiri::XML::Element.new(name, @doc)
402
+ element = @doc.create_element(name)
395
403
  attrs.each { |k, v| element[k.to_s] = v }
396
404
  element
397
405
  end
398
406
 
399
407
  def mi(content)
400
408
  e = el("mi", mathvariant: "normal")
401
- e.content = content.to_s
409
+ e.add_child(@doc.create_text(content.to_s))
402
410
  e
403
411
  end
404
412
 
405
413
  def mn(content)
406
- e = el("mn"); e.content = content.to_s; e
414
+ e = el("mn"); e.add_child(@doc.create_text(content.to_s)); e
407
415
  end
408
416
 
409
417
  def mo(content)
410
- e = el("mo"); e.content = content.to_s; e
418
+ e = el("mo"); e.add_child(@doc.create_text(content.to_s)); e
411
419
  end
412
420
 
413
421
  def mtext(content)
414
- e = el("mtext"); e.content = content.to_s; e
422
+ e = el("mtext"); e.add_child(@doc.create_text(content.to_s)); e
415
423
  end
416
424
 
417
425
  # -- Beyond-formulas helpers -----------------------------------
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogiri'
3
+ require 'moxml'
4
4
 
5
5
  module AsciiChem
6
6
  module Formatter
@@ -88,7 +88,8 @@ module AsciiChem
88
88
  end
89
89
 
90
90
  def render_svg(result)
91
- doc = Nokogiri::XML::Document.new
91
+ doc = Moxml.new.create_document
92
+ doc.add_child(doc.create_declaration('1.0', nil, nil))
92
93
  svg = build_svg_root(doc, result)
93
94
  bonds_first_then_atoms(result, doc, svg)
94
95
  doc.root = svg
@@ -96,14 +97,14 @@ module AsciiChem
96
97
  end
97
98
 
98
99
  def build_svg_root(doc, result)
99
- svg = Nokogiri::XML::Element.new('svg', doc)
100
+ svg = doc.create_element('svg')
100
101
  svg['xmlns'] = 'http://www.w3.org/2000/svg'
101
102
  svg['width'] = result.width.to_s
102
103
  svg['height'] = result.height.to_s
103
104
  svg['viewBox'] = "0 0 #{result.width} #{result.height}"
104
105
  svg['role'] = 'img'
105
- title = Nokogiri::XML::Element.new('title', doc)
106
- title.content = title_text(result)
106
+ title = doc.create_element('title')
107
+ title.add_child(doc.create_text(title_text(result)))
107
108
  svg.add_child(title)
108
109
  svg
109
110
  end
@@ -136,7 +137,7 @@ module AsciiChem
136
137
  end
137
138
 
138
139
  def render_into(parent)
139
- group = Nokogiri::XML::Element.new('g', @doc)
140
+ group = @doc.create_element('g')
140
141
  group.add_child(circle)
141
142
  group.add_child(label)
142
143
  parent.add_child(group)
@@ -145,7 +146,7 @@ module AsciiChem
145
146
  private
146
147
 
147
148
  def circle
148
- el = Nokogiri::XML::Element.new('circle', @doc)
149
+ el = @doc.create_element('circle')
149
150
  el['cx'] = @atom.x.to_s
150
151
  el['cy'] = @atom.y.to_s
151
152
  el['r'] = StructuralSvg::ATOM_RADIUS.to_s
@@ -156,14 +157,14 @@ module AsciiChem
156
157
  end
157
158
 
158
159
  def label
159
- el = Nokogiri::XML::Element.new('text', @doc)
160
+ el = @doc.create_element('text')
160
161
  el['x'] = @atom.x.to_s
161
162
  el['y'] = (@atom.y + 4).to_s
162
163
  el['text-anchor'] = 'middle'
163
164
  el['font-family'] = 'serif'
164
165
  el['font-size'] = '14'
165
166
  el['fill'] = color
166
- el.content = @atom.element
167
+ el.add_child(@doc.create_text(@atom.element))
167
168
  el
168
169
  end
169
170
 
@@ -176,7 +177,7 @@ module AsciiChem
176
177
  # Renders a single bond between two positioned atoms. Dispatches
177
178
  # on bond kind via a registry of Procs. Each Proc receives the
178
179
  # renderer (for its public line/offset helpers) and returns an
179
- # array of Nokogiri elements. Adding a new bond style is a new
180
+ # array of moxml elements. Adding a new bond style is a new
180
181
  # Proc + one registry entry — no edits to existing renderers.
181
182
  class BondRenderer
182
183
  def initialize(doc, from_atom, to_atom, kind)
@@ -210,7 +211,7 @@ module AsciiChem
210
211
  end
211
212
 
212
213
  def line(start_x, start_y, end_x, end_y)
213
- el = Nokogiri::XML::Element.new('line', @doc)
214
+ el = @doc.create_element('line')
214
215
  el['x1'] = start_x.to_s
215
216
  el['y1'] = start_y.to_s
216
217
  el['x2'] = end_x.to_s
@@ -221,7 +222,7 @@ module AsciiChem
221
222
  end
222
223
 
223
224
  def polygon(points, fill:)
224
- el = Nokogiri::XML::Element.new('polygon', @doc)
225
+ el = @doc.create_element('polygon')
225
226
  el['points'] = points.map { |x, y| "#{x},#{y}" }.join(' ')
226
227
  el['fill'] = fill
227
228
  el
@@ -247,7 +248,7 @@ module AsciiChem
247
248
  # -- Strategy registry --------------------------------------
248
249
  #
249
250
  # Each entry maps a bond kind symbol to a Proc that takes the
250
- # renderer and returns an array of Nokogiri elements. Procs
251
+ # renderer and returns an array of moxml elements. Procs
251
252
  # use the public helpers above; nothing reaches into private
252
253
  # state.
253
254
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.29.2"
4
+ VERSION = "0.30.0"
5
5
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Internal XML helper. Currently a thin marker module kept for parity
4
- # with future formatters that need XML construction without Nokogiri
5
- # (e.g. JRuby). The Mathml formatter uses Nokogiri directly.
4
+ # with future formatters that need XML construction without a hard XML engine
5
+ # dependency. The Mathml formatter uses moxml directly.
6
6
  module AsciiChem
7
7
  module XmlBuilder
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciichem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.2
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -41,22 +41,16 @@ dependencies:
41
41
  name: lutaml-model
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '0.8'
47
- - - "<"
48
- - !ruby/object:Gem::Version
49
- version: '2'
50
47
  type: :runtime
51
48
  prerelease: false
52
49
  version_requirements: !ruby/object:Gem::Requirement
53
50
  requirements:
54
- - - ">="
51
+ - - "~>"
55
52
  - !ruby/object:Gem::Version
56
53
  version: '0.8'
57
- - - "<"
58
- - !ruby/object:Gem::Version
59
- version: '2'
60
54
  - !ruby/object:Gem::Dependency
61
55
  name: mml
62
56
  requirement: !ruby/object:Gem::Requirement
@@ -72,19 +66,19 @@ dependencies:
72
66
  - !ruby/object:Gem::Version
73
67
  version: '2.3'
74
68
  - !ruby/object:Gem::Dependency
75
- name: nokogiri
69
+ name: moxml
76
70
  requirement: !ruby/object:Gem::Requirement
77
71
  requirements:
78
72
  - - "~>"
79
73
  - !ruby/object:Gem::Version
80
- version: '1.16'
74
+ version: '0.5'
81
75
  type: :runtime
82
76
  prerelease: false
83
77
  version_requirements: !ruby/object:Gem::Requirement
84
78
  requirements:
85
79
  - - "~>"
86
80
  - !ruby/object:Gem::Version
87
- version: '1.16'
81
+ version: '0.5'
88
82
  - !ruby/object:Gem::Dependency
89
83
  name: parslet
90
84
  requirement: !ruby/object:Gem::Requirement
@@ -103,36 +97,30 @@ dependencies:
103
97
  name: relaton-bib
104
98
  requirement: !ruby/object:Gem::Requirement
105
99
  requirements:
106
- - - ">="
107
- - !ruby/object:Gem::Version
108
- version: '0.1'
109
- - - "<"
100
+ - - "~>"
110
101
  - !ruby/object:Gem::Version
111
- version: '3'
102
+ version: '2.1'
112
103
  type: :runtime
113
104
  prerelease: false
114
105
  version_requirements: !ruby/object:Gem::Requirement
115
106
  requirements:
116
- - - ">="
117
- - !ruby/object:Gem::Version
118
- version: '0.1'
119
- - - "<"
107
+ - - "~>"
120
108
  - !ruby/object:Gem::Version
121
- version: '3'
109
+ version: '2.1'
122
110
  - !ruby/object:Gem::Dependency
123
111
  name: plurimath
124
112
  requirement: !ruby/object:Gem::Requirement
125
113
  requirements:
126
114
  - - "~>"
127
115
  - !ruby/object:Gem::Version
128
- version: '0.8'
116
+ version: '0.11'
129
117
  type: :runtime
130
118
  prerelease: false
131
119
  version_requirements: !ruby/object:Gem::Requirement
132
120
  requirements:
133
121
  - - "~>"
134
122
  - !ruby/object:Gem::Version
135
- version: '0.8'
123
+ version: '0.11'
136
124
  - !ruby/object:Gem::Dependency
137
125
  name: thor
138
126
  requirement: !ruby/object:Gem::Requirement