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 +4 -4
- data/CHANGELOG.md +23 -0
- data/Gemfile +1 -0
- data/asciichem.gemspec +6 -4
- data/benchmarks/README.md +62 -0
- data/lib/asciichem/cml/conditions_extensions.rb +4 -4
- data/lib/asciichem/cml/extensions/atom_attributes.rb +4 -4
- data/lib/asciichem/cml/extensions/top_level.rb +5 -5
- data/lib/asciichem/cml/extensions.rb +1 -1
- data/lib/asciichem/cml/group_extensions.rb +3 -3
- data/lib/asciichem/cml/metadata_extensions.rb +6 -6
- data/lib/asciichem/cml/opaque_extensions.rb +10 -12
- data/lib/asciichem/formatter/mathml.rb +22 -14
- data/lib/asciichem/formatter/structural_svg.rb +14 -13
- data/lib/asciichem/version.rb +1 -1
- data/lib/asciichem/xml_builder.rb +2 -2
- metadata +12 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf3a968f977add504a84afdbeb3d5d231b959688f4d9c9ac70e424a251ea2f75
|
|
4
|
+
data.tar.gz: 8b8c12366ce9f05f44058b897b474f226dea561568ee6c3e17820dda4c5bd4f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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", "
|
|
40
|
+
spec.add_dependency "lutaml-model", "~> 0.8"
|
|
39
41
|
spec.add_dependency "mml", "~> 2.3"
|
|
40
|
-
spec.add_dependency "
|
|
42
|
+
spec.add_dependency "moxml", "~> 0.5"
|
|
41
43
|
spec.add_dependency "parslet", "~> 2.0"
|
|
42
|
-
spec.add_dependency "relaton-bib", "
|
|
43
|
-
spec.add_dependency "plurimath", "~> 0.
|
|
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 '
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 '
|
|
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 =
|
|
42
|
+
doc = Moxml.parse(xml)
|
|
43
43
|
root = doc.root
|
|
44
|
-
unless root.namespaces.
|
|
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 =
|
|
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 '
|
|
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 =
|
|
123
|
+
doc = Moxml.parse(xml)
|
|
124
124
|
root = doc.root
|
|
125
|
-
unless root.namespaces.
|
|
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 =
|
|
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.
|
|
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.
|
|
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 '
|
|
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 =
|
|
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 =
|
|
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 '
|
|
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 =
|
|
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 =
|
|
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 |
|
|
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 "
|
|
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 =
|
|
45
|
+
doc = Moxml.parse(xml)
|
|
46
46
|
root = doc.root
|
|
47
|
-
wire_children = root.
|
|
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
|
-
|
|
57
|
+
nodes = Moxml.new.parse_fragment(raw_xml)
|
|
58
58
|
if wire_index >= wire_children.length
|
|
59
|
-
root.add_child(
|
|
59
|
+
nodes.each { |n| root.add_child(n) }
|
|
60
60
|
else
|
|
61
|
-
wire_children[wire_index].add_previous_sibling(
|
|
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 =
|
|
91
|
+
doc = Moxml.parse(xml)
|
|
92
92
|
root = doc.root
|
|
93
93
|
result = []
|
|
94
|
-
children = root.
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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 =
|
|
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 =
|
|
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 ⇌.
|
|
31
|
-
|
|
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(
|
|
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 =
|
|
215
|
-
|
|
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,
|
|
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
|
-
# --
|
|
399
|
+
# -- Element factories ------------------------------------
|
|
392
400
|
|
|
393
401
|
def el(name, attrs = {})
|
|
394
|
-
element =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 '
|
|
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 =
|
|
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 =
|
|
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 =
|
|
106
|
-
title.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
data/lib/asciichem/version.rb
CHANGED
|
@@ -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
|
|
5
|
-
#
|
|
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.
|
|
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:
|
|
69
|
+
name: moxml
|
|
76
70
|
requirement: !ruby/object:Gem::Requirement
|
|
77
71
|
requirements:
|
|
78
72
|
- - "~>"
|
|
79
73
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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.
|
|
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.
|
|
123
|
+
version: '0.11'
|
|
136
124
|
- !ruby/object:Gem::Dependency
|
|
137
125
|
name: thor
|
|
138
126
|
requirement: !ruby/object:Gem::Requirement
|