asciichem 0.3.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 +7 -0
- data/.github/workflows/ci.yml +21 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +50 -0
- data/ARCHITECTURE.adoc +239 -0
- data/CHANGELOG.md +17 -0
- data/CLAUDE.md +318 -0
- data/Gemfile +14 -0
- data/LICENSE +24 -0
- data/README.adoc +55 -0
- data/RELEASING.md +102 -0
- data/Rakefile +8 -0
- data/asciichem.gemspec +39 -0
- data/benchmarks/RESULTS.md +49 -0
- data/benchmarks/benchmark.rb +106 -0
- data/exe/asciichem +6 -0
- data/lib/asciichem/cli.rb +96 -0
- data/lib/asciichem/cml/extensions.rb +360 -0
- data/lib/asciichem/cml/group_extensions.rb +290 -0
- data/lib/asciichem/cml/translator.rb +69 -0
- data/lib/asciichem/cml.rb +40 -0
- data/lib/asciichem/errors.rb +9 -0
- data/lib/asciichem/formatter/base.rb +21 -0
- data/lib/asciichem/formatter/html.rb +111 -0
- data/lib/asciichem/formatter/latex.rb +173 -0
- data/lib/asciichem/formatter/mathml.rb +309 -0
- data/lib/asciichem/formatter/structural_svg.rb +286 -0
- data/lib/asciichem/formatter/svg.rb +141 -0
- data/lib/asciichem/formatter/text.rb +143 -0
- data/lib/asciichem/formatter.rb +43 -0
- data/lib/asciichem/grammar.rb +344 -0
- data/lib/asciichem/greek.rb +80 -0
- data/lib/asciichem/layout.rb +313 -0
- data/lib/asciichem/linter/balance_check.rb +103 -0
- data/lib/asciichem/linter/base.rb +52 -0
- data/lib/asciichem/linter/bracket_balance_check.rb +44 -0
- data/lib/asciichem/linter/diagnostic.rb +16 -0
- data/lib/asciichem/linter/element_validation_check.rb +39 -0
- data/lib/asciichem/linter/isotope_sanity_check.rb +47 -0
- data/lib/asciichem/linter/registry.rb +31 -0
- data/lib/asciichem/linter/unclosed_ring_check.rb +27 -0
- data/lib/asciichem/linter/valence_check.rb +91 -0
- data/lib/asciichem/linter.rb +40 -0
- data/lib/asciichem/model/atom.rb +73 -0
- data/lib/asciichem/model/bond.rb +41 -0
- data/lib/asciichem/model/electron_configuration.rb +36 -0
- data/lib/asciichem/model/embedded_math.rb +26 -0
- data/lib/asciichem/model/formula.rb +31 -0
- data/lib/asciichem/model/group.rb +50 -0
- data/lib/asciichem/model/identifier.rb +14 -0
- data/lib/asciichem/model/molecule.rb +70 -0
- data/lib/asciichem/model/name.rb +15 -0
- data/lib/asciichem/model/node.rb +100 -0
- data/lib/asciichem/model/reaction.rb +49 -0
- data/lib/asciichem/model/reaction_cascade.rb +33 -0
- data/lib/asciichem/model/text.rb +23 -0
- data/lib/asciichem/model.rb +23 -0
- data/lib/asciichem/model_adapter/from_canonical.rb +272 -0
- data/lib/asciichem/model_adapter/to_canonical.rb +429 -0
- data/lib/asciichem/model_adapter.rb +56 -0
- data/lib/asciichem/parser.rb +36 -0
- data/lib/asciichem/periodic_table.rb +112 -0
- data/lib/asciichem/ring_bonds.rb +68 -0
- data/lib/asciichem/transform.rb +513 -0
- data/lib/asciichem/version.rb +5 -0
- data/lib/asciichem/xml_builder.rb +9 -0
- data/lib/asciichem.rb +34 -0
- metadata +197 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "chemicalml"
|
|
4
|
+
|
|
5
|
+
module AsciiChem
|
|
6
|
+
module Cml
|
|
7
|
+
# Thin composition of the AsciiChem <-> canonical adapter
|
|
8
|
+
# (`AsciiChem::ModelAdapter`) and the canonical <-> CML adapter
|
|
9
|
+
# (`Chemicalml::Cml::Translator`). Owns no chemistry logic — just
|
|
10
|
+
# wiring, the schema-registration dance that chemicalml requires
|
|
11
|
+
# on first use, and the aci: extension namespace side-channel
|
|
12
|
+
# that carries AsciiChem-specific fields through CML round-trip.
|
|
13
|
+
class Translator
|
|
14
|
+
class << self
|
|
15
|
+
# AsciiChem::Model::Formula -> CML XML string.
|
|
16
|
+
def from_asciichem(formula)
|
|
17
|
+
ensure_schema_registered!
|
|
18
|
+
translation = AsciiChem::ModelAdapter.to_canonical_with_mapping(formula)
|
|
19
|
+
wire_doc = Chemicalml::Cml::Translator.from_canonical(translation.document)
|
|
20
|
+
xml = wire_doc.to_xml
|
|
21
|
+
xml = inject_atom_extensions(xml, translation.atom_mapping)
|
|
22
|
+
xml = inject_group_extensions(xml, translation.groups)
|
|
23
|
+
inject_top_level_extensions(xml, formula)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# CML XML string -> AsciiChem::Model::Formula.
|
|
27
|
+
def to_asciichem(xml)
|
|
28
|
+
ensure_schema_registered!
|
|
29
|
+
top_level = Extensions.extract_top_level(xml)
|
|
30
|
+
atom_extensions = Extensions.extract(xml)
|
|
31
|
+
group_extensions = GroupExtensions.extract(xml)
|
|
32
|
+
wire_doc = Chemicalml::Cml::Document.from_xml(xml)
|
|
33
|
+
canonical = Chemicalml::Cml::Translator.to_canonical(wire_doc)
|
|
34
|
+
formula = AsciiChem::ModelAdapter.from_canonical(canonical)
|
|
35
|
+
Extensions.restore(formula, canonical, atom_extensions)
|
|
36
|
+
GroupExtensions.restore(formula, canonical, group_extensions)
|
|
37
|
+
Extensions.restore_top_level(formula, top_level)
|
|
38
|
+
formula
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def inject_atom_extensions(xml, atom_mapping)
|
|
44
|
+
extensions = Extensions.collect(atom_mapping)
|
|
45
|
+
Extensions.inject(xml, extensions)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inject_group_extensions(xml, groups_by_molecule)
|
|
49
|
+
collected = GroupExtensions.collect(groups_by_molecule)
|
|
50
|
+
GroupExtensions.inject(xml, collected)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def inject_top_level_extensions(xml, formula)
|
|
54
|
+
top_level = Extensions.collect_top_level(formula)
|
|
55
|
+
Extensions.inject_top_level(xml, top_level)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# chemicalml registers wire classes into a lutaml-model
|
|
59
|
+
# TypeRegistry on first use. The registry call is idempotent
|
|
60
|
+
# (guarded by `@models_registered`) but must run before any
|
|
61
|
+
# `from_xml` so element types like `molecule` and `atom` are
|
|
62
|
+
# resolvable.
|
|
63
|
+
def ensure_schema_registered!
|
|
64
|
+
Chemicalml::Cml::Schema3.ensure_registered!
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
# CML (Chemical Markup Language) support for AsciiChem.
|
|
5
|
+
#
|
|
6
|
+
# Bidirectional conversion between the AsciiChem text world and the
|
|
7
|
+
# CML XML world. The pipeline is:
|
|
8
|
+
#
|
|
9
|
+
# AsciiChem::Model <-> AsciiChem::ModelAdapter <-> Chemicalml::Model
|
|
10
|
+
# ^
|
|
11
|
+
# |
|
|
12
|
+
# Chemicalml::Cml::Translator
|
|
13
|
+
# |
|
|
14
|
+
# v
|
|
15
|
+
# Chemicalml::Cml::* (wire)
|
|
16
|
+
#
|
|
17
|
+
# The canonical model (Chemicalml::Model) is the format-agnostic
|
|
18
|
+
# hub. AsciiChem and CML each have their own adapter; the adapters
|
|
19
|
+
# never talk to each other directly. Adding a new format (SMILES,
|
|
20
|
+
# InChI, MOL) is a new adapter — none of the existing code changes.
|
|
21
|
+
#
|
|
22
|
+
# Public API:
|
|
23
|
+
# AsciiChem::Cml.from_asciichem(formula) # => CML XML string
|
|
24
|
+
# AsciiChem::Cml.parse(xml) # => AsciiChem::Model::Formula
|
|
25
|
+
module Cml
|
|
26
|
+
autoload :Extensions, "asciichem/cml/extensions"
|
|
27
|
+
autoload :GroupExtensions, "asciichem/cml/group_extensions"
|
|
28
|
+
autoload :Translator, "asciichem/cml/translator"
|
|
29
|
+
|
|
30
|
+
# Serialise an AsciiChem::Model::Formula to CML XML.
|
|
31
|
+
def self.from_asciichem(formula)
|
|
32
|
+
Translator.from_asciichem(formula)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Parse CML XML into an AsciiChem::Model::Formula.
|
|
36
|
+
def self.parse(xml)
|
|
37
|
+
Translator.to_asciichem(xml)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Formatter
|
|
5
|
+
# Visitor base class. Subclasses implement one `visit_<node_name>`
|
|
6
|
+
# method per Model::Node subclass. Missing visits raise
|
|
7
|
+
# NotImplementedError so gaps surface at first use, not silently.
|
|
8
|
+
class Base
|
|
9
|
+
def render(node)
|
|
10
|
+
node.accept(self)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def not_implemented_for(node)
|
|
16
|
+
raise NotImplementedError,
|
|
17
|
+
"#{self.class} cannot render #{node.class.name}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Formatter
|
|
5
|
+
# Renders a Model tree as inline HTML using `<sub>` and `<sup>` tags.
|
|
6
|
+
# Suitable for inline display in pages that don't want to load
|
|
7
|
+
# MathJax or render MathML.
|
|
8
|
+
#
|
|
9
|
+
# Example: `H_2O` -> <code>H<sub>2</sub>O</code>.
|
|
10
|
+
class Html < Base
|
|
11
|
+
def visit_formula(formula)
|
|
12
|
+
inner = formula.nodes.map { |n| render_node(n) }.join
|
|
13
|
+
"<span class=\"asciichem\">#{inner}</span>"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def visit_molecule(molecule)
|
|
17
|
+
prefix = molecule.coefficient.nil? || molecule.coefficient.to_s.empty? ? "" : "#{escape(molecule.coefficient)}"
|
|
18
|
+
stereo = molecule.stereo ? "(#{escape(molecule.stereo_letter)})-" : ""
|
|
19
|
+
body = molecule.nodes.map { |n| render_node(n) }.join
|
|
20
|
+
"#{stereo}#{prefix}#{body}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visit_atom(atom)
|
|
24
|
+
parts = []
|
|
25
|
+
parts << "<sup>#{escape(atom.isotope)}</sup>" if atom.isotope
|
|
26
|
+
parts << escape(atom.element)
|
|
27
|
+
parts << "<sub>#{escape(atom.subscript)}</sub>" if atom.subscript
|
|
28
|
+
parts << "<sup>#{escape(atom.superscript)}</sup>" if atom.superscript
|
|
29
|
+
parts << "<sup>#{escape(atom.charge)}</sup>" if atom.charge
|
|
30
|
+
if atom.oxidation_state
|
|
31
|
+
parts << "<sup>(#{escape(atom.oxidation_state)})</sup>"
|
|
32
|
+
end
|
|
33
|
+
parts << escape(atom.ring_closures) if atom.ring_closures
|
|
34
|
+
parts.join
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def visit_group(group)
|
|
38
|
+
body = group.nodes.map { |n| render_node(n) }.join
|
|
39
|
+
suffix = group.multiplicity ? "<sub>#{escape(group.multiplicity)}</sub>" : ""
|
|
40
|
+
"#{escape(group.open_char)}#{body}#{escape(group.close_char)}#{suffix}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def visit_bond(bond)
|
|
44
|
+
escape(bond.ascii)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def visit_reaction(reaction)
|
|
48
|
+
left = reaction.reactants.map { |n| render_node(n) }.join(" + ")
|
|
49
|
+
right = reaction.products.map { |n| render_node(n) }.join(" + ")
|
|
50
|
+
arrow = escape(reaction.arrow_entity)
|
|
51
|
+
conds = reaction.conditions
|
|
52
|
+
return "#{left} #{arrow} #{right}" unless conds
|
|
53
|
+
|
|
54
|
+
above = conds.above ? "<sup>#{escape(conds.above)}</sup>" : ""
|
|
55
|
+
below = conds.below ? "<sub>#{escape(conds.below)}</sub>" : ""
|
|
56
|
+
"#{left} #{above}#{arrow}#{below} #{right}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def visit_reaction_cascade(cascade)
|
|
60
|
+
return "" if cascade.steps.empty?
|
|
61
|
+
|
|
62
|
+
head = cascade.steps.first
|
|
63
|
+
out = head.reactants.map { |n| render_node(n) }.join(" + ")
|
|
64
|
+
cascade.steps.each do |step|
|
|
65
|
+
arrow = escape(step.arrow_entity)
|
|
66
|
+
conds = step.conditions
|
|
67
|
+
if conds
|
|
68
|
+
above = conds.above ? "<sup>#{escape(conds.above)}</sup>" : ""
|
|
69
|
+
below = conds.below ? "<sub>#{escape(conds.below)}</sub>" : ""
|
|
70
|
+
out += " #{above}#{arrow}#{below}"
|
|
71
|
+
else
|
|
72
|
+
out += " #{arrow}"
|
|
73
|
+
end
|
|
74
|
+
out += " " + step.products.map { |n| render_node(n) }.join(" + ")
|
|
75
|
+
end
|
|
76
|
+
out
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def visit_electron_configuration(ec)
|
|
80
|
+
ec.orbitals.map { |orb, occ| "#{escape(orb)}<sup>#{escape(occ)}</sup>" }.join(" ")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def visit_embedded_math(em)
|
|
84
|
+
em.formula.to_mathml
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def visit_text(text)
|
|
88
|
+
escape(text.content)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def render_node(node)
|
|
94
|
+
node.accept(self)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Minimal HTML escaper. The model content is plain text (digits,
|
|
98
|
+
# element symbols, conditions text) so we only need the four
|
|
99
|
+
# XML-mandated characters.
|
|
100
|
+
def escape(string)
|
|
101
|
+
return "" if string.nil?
|
|
102
|
+
|
|
103
|
+
string.to_s
|
|
104
|
+
.gsub("&", "&")
|
|
105
|
+
.gsub("<", "<")
|
|
106
|
+
.gsub(">", ">")
|
|
107
|
+
.gsub('"', """)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Formatter
|
|
5
|
+
# Renders a Model tree as LaTeX, wrapped in `mhchem`'s `\ce{...}` at
|
|
6
|
+
# the formula level only. Output requires `mhchem` in the LaTeX
|
|
7
|
+
# preamble:
|
|
8
|
+
#
|
|
9
|
+
# \\usepackage[version=4]{mhchem}
|
|
10
|
+
#
|
|
11
|
+
# Example: `H_2O` -> `\ce{H2O}`.
|
|
12
|
+
class Latex < Base
|
|
13
|
+
def initialize
|
|
14
|
+
@inside_ce = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def visit_formula(formula)
|
|
18
|
+
within_ce { "\\ce{" + formula.nodes.map { |n| render_node(n) }.join + "}" }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def visit_molecule(molecule)
|
|
22
|
+
prefix = blank?(molecule.coefficient) ? "" : molecule.coefficient.to_s
|
|
23
|
+
stereo = molecule.stereo ? "(#{molecule.stereo_letter})-" : ""
|
|
24
|
+
body = molecule.nodes.map { |n| render_node(n) }.join
|
|
25
|
+
if @inside_ce
|
|
26
|
+
"#{stereo}#{prefix}#{body}"
|
|
27
|
+
else
|
|
28
|
+
"\\ce{#{stereo}#{prefix}#{body}}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def visit_atom(atom)
|
|
33
|
+
parts = []
|
|
34
|
+
parts << (":" * atom.lone_pairs) if atom.lone_pairs
|
|
35
|
+
parts << "^#{wrap(atom.isotope)}" if atom.isotope
|
|
36
|
+
parts << wrap(atom.element)
|
|
37
|
+
parts << mhchem_subscript(atom.subscript) if atom.subscript
|
|
38
|
+
if atom.charge
|
|
39
|
+
parts << "^#{wrap(atom.charge)}"
|
|
40
|
+
elsif atom.oxidation_state
|
|
41
|
+
parts << "^{(#{wrap(atom.oxidation_state)})}"
|
|
42
|
+
elsif atom.superscript
|
|
43
|
+
parts << "^#{wrap(atom.superscript)}"
|
|
44
|
+
end
|
|
45
|
+
parts << ("." * atom.radical_electrons) if atom.radical_electrons
|
|
46
|
+
parts << atom.ring_closures.to_s if atom.ring_closures
|
|
47
|
+
parts.join
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def visit_group(group)
|
|
51
|
+
body = group.nodes.map { |n| render_node(n) }.join
|
|
52
|
+
open, close = bracket_chars(group.bracket)
|
|
53
|
+
suffix = group.multiplicity ? mhchem_subscript(group.multiplicity) : ""
|
|
54
|
+
"#{open}#{body}#{close}#{suffix}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def visit_bond(bond)
|
|
58
|
+
case bond.kind
|
|
59
|
+
when :single then "-"
|
|
60
|
+
when :double then "="
|
|
61
|
+
when :triple then "\\\\equiv{}"
|
|
62
|
+
else bond.ascii
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def visit_reaction(reaction)
|
|
67
|
+
left = reaction.reactants.map { |n| render_node(n) }.join(" + ")
|
|
68
|
+
right = reaction.products.map { |n| render_node(n) }.join(" + ")
|
|
69
|
+
arrow = arrow_for(reaction.arrow)
|
|
70
|
+
conds = reaction.conditions
|
|
71
|
+
return "#{left} #{arrow} #{right}" unless conds
|
|
72
|
+
|
|
73
|
+
above = conditions_bracket(conds.above)
|
|
74
|
+
below = conditions_bracket(conds.below)
|
|
75
|
+
"#{left} #{arrow}#{above}#{below} #{right}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def visit_reaction_cascade(cascade)
|
|
79
|
+
return "" if cascade.steps.empty?
|
|
80
|
+
|
|
81
|
+
head = cascade.steps.first
|
|
82
|
+
out = head.reactants.map { |n| render_node(n) }.join(" + ")
|
|
83
|
+
cascade.steps.each do |step|
|
|
84
|
+
arrow = arrow_for(step.arrow)
|
|
85
|
+
conds = step.conditions
|
|
86
|
+
if conds
|
|
87
|
+
above = conditions_bracket(conds.above)
|
|
88
|
+
below = conditions_bracket(conds.below)
|
|
89
|
+
out += " #{arrow}#{above}#{below}"
|
|
90
|
+
else
|
|
91
|
+
out += " #{arrow}"
|
|
92
|
+
end
|
|
93
|
+
out += " " + step.products.map { |n| render_node(n) }.join(" + ")
|
|
94
|
+
end
|
|
95
|
+
out
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def visit_electron_configuration(ec)
|
|
99
|
+
inner = ec.orbitals.map { |orb, occ| "#{wrap(orb)}^#{wrap(occ)}" }.join(" ")
|
|
100
|
+
"\\ce{#{inner}}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def visit_embedded_math(em)
|
|
104
|
+
"$#{em.formula.to_latex}$"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def visit_text(text)
|
|
108
|
+
text.content
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def render_node(node)
|
|
114
|
+
node.accept(self)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def within_ce
|
|
118
|
+
prev = @inside_ce
|
|
119
|
+
@inside_ce = true
|
|
120
|
+
yield
|
|
121
|
+
ensure
|
|
122
|
+
@inside_ce = prev
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# mhchem accepts bare digits after an element as subscripts. Use
|
|
126
|
+
# bare form for digit-only values; brace form for anything else.
|
|
127
|
+
def mhchem_subscript(value)
|
|
128
|
+
s = value.to_s
|
|
129
|
+
return s if s.match?(/\A\d+\z/)
|
|
130
|
+
|
|
131
|
+
"_{#{s}}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# mhchem accepts bare digits for multi-digit values (`^14`, `_12`).
|
|
135
|
+
# Use braces only for non-digit values to keep output idiomatic.
|
|
136
|
+
def wrap(value)
|
|
137
|
+
s = value.to_s
|
|
138
|
+
return s if s.match?(/\A[a-zA-Z0-9]+\z/)
|
|
139
|
+
|
|
140
|
+
"{#{s}}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def blank?(value)
|
|
144
|
+
value.nil? || value.to_s.empty?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def bracket_chars(kind)
|
|
148
|
+
case kind
|
|
149
|
+
when :paren then ["(", ")"]
|
|
150
|
+
when :square then ["[", "]"]
|
|
151
|
+
when :brace then ["{", "}"]
|
|
152
|
+
else ["(", ")"]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def arrow_for(kind)
|
|
157
|
+
case kind
|
|
158
|
+
when :forward then "->"
|
|
159
|
+
when :reverse then "<-"
|
|
160
|
+
when :equilibrium then "<=>"
|
|
161
|
+
when :resonance then "<->"
|
|
162
|
+
else "->"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def conditions_bracket(value)
|
|
167
|
+
return "" if value.nil? || value.to_s.empty?
|
|
168
|
+
|
|
169
|
+
"[#{value}]"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|