asciichem 0.29.3 → 0.30.1
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 +26 -0
- data/asciichem.gemspec +1 -1
- 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 +38 -9
- 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 +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f8a0ef9952b9507c35ded04295298a3a62e86a7efce1f92d258a1aaf6d6125c
|
|
4
|
+
data.tar.gz: 0d11ccfb16383194e8a1fd3bf0816ce8b8465b548bc6dac43535d2633a0cf2ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 270015bf0f88f0bc2f06452a472759970b39ab75ae9aef233ea3c9f94f7b3abc2d20752098151aedd7ab51251aaad9414cfdb349167c059ac036e8bbdaff04d5
|
|
7
|
+
data.tar.gz: a4211637fc504c7db50994d14b151bb5e45357b48d870b4f4aff7eb49c79c269ba7014a755fd00c6e8dd22678e1f044011b9b4123ecc24530b9d693464a11357
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
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.1] - 2026-09-23
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- Groups attached by an explicit bond no longer re-spell through
|
|
10
|
+
CML round-trip: `CC(=O)O` came back as `CC=(O)O`. The CML
|
|
11
|
+
rebuilder left the attachment bond outside the reconstructed
|
|
12
|
+
group; the grammar keeps it inside (the inner molecule starts
|
|
13
|
+
with the bond), so the rebuilder now pulls an adjacent preceding
|
|
14
|
+
Bond into the group. Affects text syntax and SMILES ingestion
|
|
15
|
+
alike; regression specs cover single/sibling bonded groups,
|
|
16
|
+
multi-atom groups, and aspirin stability across double
|
|
17
|
+
round-trips.
|
|
18
|
+
|
|
19
|
+
## [0.30.0] - 2026-09-22
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- The XML layer migrates from Nokogiri to
|
|
23
|
+
[moxml](https://github.com/lutaml/moxml) — the adapter layer the
|
|
24
|
+
rest of the lutaml ecosystem (chemicalml, CML wire, metanorma
|
|
25
|
+
gems) already uses, so AsciiChem follows the consumer's
|
|
26
|
+
configured XML backend instead of pinning one. The gemspec
|
|
27
|
+
dependency changes accordingly (`nokogiri ~> 1.18` out, `moxml
|
|
28
|
+
~> 0.5` in). MathML output is byte-identical to 0.29.3: the L2
|
|
29
|
+
corpus goldens pass unchanged, including empty-element
|
|
30
|
+
compaction and the `<?xml?>` declaration.
|
|
31
|
+
|
|
6
32
|
## [0.29.3] - 2026-09-22
|
|
7
33
|
|
|
8
34
|
### Changed
|
data/asciichem.gemspec
CHANGED
|
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
|
|
|
39
39
|
spec.add_dependency "elkrb", "~> 1.0"
|
|
40
40
|
spec.add_dependency "lutaml-model", "~> 0.8"
|
|
41
41
|
spec.add_dependency "mml", "~> 2.3"
|
|
42
|
-
spec.add_dependency "
|
|
42
|
+
spec.add_dependency "moxml", "~> 0.5"
|
|
43
43
|
spec.add_dependency "parslet", "~> 2.0"
|
|
44
44
|
spec.add_dependency "relaton-bib", "~> 2.1"
|
|
45
45
|
spec.add_dependency "plurimath", "~> 0.11"
|
|
@@ -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",
|
|
@@ -213,18 +213,23 @@ module AsciiChem
|
|
|
213
213
|
def self.splice_group_into(nodes, target_nodes, record)
|
|
214
214
|
return if target_nodes.empty?
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
inner_nodes = group_nodes(nodes, target_nodes)
|
|
217
|
+
|
|
218
|
+
positions = target_nodes
|
|
219
|
+
.map { |target| position_of(nodes, target) }
|
|
220
|
+
.compact
|
|
220
221
|
return if positions.empty?
|
|
221
222
|
|
|
222
223
|
group = AsciiChem::Model::Group.new(
|
|
223
|
-
nodes:
|
|
224
|
+
nodes: inner_nodes,
|
|
224
225
|
multiplicity: record[:multiplicity],
|
|
225
226
|
bracket: record[:bracket]
|
|
226
227
|
)
|
|
228
|
+
replace_positions_with_group(nodes, group, positions)
|
|
229
|
+
end
|
|
230
|
+
private_class_method :splice_group_into
|
|
227
231
|
|
|
232
|
+
def self.replace_positions_with_group(nodes, group, positions)
|
|
228
233
|
first_pos = positions.min
|
|
229
234
|
nodes[first_pos] = group
|
|
230
235
|
# Remove remaining positions in descending order so earlier
|
|
@@ -233,7 +238,31 @@ module AsciiChem
|
|
|
233
238
|
nodes.delete_at(pos)
|
|
234
239
|
end
|
|
235
240
|
end
|
|
236
|
-
private_class_method :
|
|
241
|
+
private_class_method :replace_positions_with_group
|
|
242
|
+
|
|
243
|
+
# A Bond sitting immediately before the group's first target node
|
|
244
|
+
# is the group's attachment bond — the `=` of `C(=O)`. The
|
|
245
|
+
# grammar keeps that bond INSIDE the group (its inner molecule
|
|
246
|
+
# starts with the bond), so the rebuilder moves it in instead of
|
|
247
|
+
# leaving a stray prefix bond outside, which would re-spell as
|
|
248
|
+
# `C=(O)`. By construction of the bond insertion (each bond sits
|
|
249
|
+
# just before its later endpoint), a bond adjacent to the group's
|
|
250
|
+
# first atom always has that atom as its later endpoint.
|
|
251
|
+
def self.group_nodes(nodes, target_nodes)
|
|
252
|
+
idx = position_of(nodes, target_nodes.first)
|
|
253
|
+
bond = idx&.positive? ? nodes[idx - 1] : nil
|
|
254
|
+
return target_nodes unless bond.is_a?(AsciiChem::Model::Bond)
|
|
255
|
+
|
|
256
|
+
[nodes.delete_at(idx - 1), *target_nodes]
|
|
257
|
+
end
|
|
258
|
+
private_class_method :group_nodes
|
|
259
|
+
|
|
260
|
+
# Identity-based position lookup so duplicate Bonds (same kind
|
|
261
|
+
# → `==` equal) aren't confused.
|
|
262
|
+
def self.position_of(nodes, node)
|
|
263
|
+
nodes.each_with_index.find { |candidate, _| candidate.equal?(node) }&.last
|
|
264
|
+
end
|
|
265
|
+
private_class_method :position_of
|
|
237
266
|
|
|
238
267
|
# Flatten the canonical document's molecules (top-level +
|
|
239
268
|
# reaction reactants/products + cascade reactions) into a list,
|
|
@@ -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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -66,19 +66,19 @@ dependencies:
|
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '2.3'
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
|
-
name:
|
|
69
|
+
name: moxml
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '
|
|
74
|
+
version: '0.5'
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '
|
|
81
|
+
version: '0.5'
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: parslet
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|