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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +21 -0
  3. data/.gitignore +14 -0
  4. data/.rubocop.yml +50 -0
  5. data/ARCHITECTURE.adoc +239 -0
  6. data/CHANGELOG.md +17 -0
  7. data/CLAUDE.md +318 -0
  8. data/Gemfile +14 -0
  9. data/LICENSE +24 -0
  10. data/README.adoc +55 -0
  11. data/RELEASING.md +102 -0
  12. data/Rakefile +8 -0
  13. data/asciichem.gemspec +39 -0
  14. data/benchmarks/RESULTS.md +49 -0
  15. data/benchmarks/benchmark.rb +106 -0
  16. data/exe/asciichem +6 -0
  17. data/lib/asciichem/cli.rb +96 -0
  18. data/lib/asciichem/cml/extensions.rb +360 -0
  19. data/lib/asciichem/cml/group_extensions.rb +290 -0
  20. data/lib/asciichem/cml/translator.rb +69 -0
  21. data/lib/asciichem/cml.rb +40 -0
  22. data/lib/asciichem/errors.rb +9 -0
  23. data/lib/asciichem/formatter/base.rb +21 -0
  24. data/lib/asciichem/formatter/html.rb +111 -0
  25. data/lib/asciichem/formatter/latex.rb +173 -0
  26. data/lib/asciichem/formatter/mathml.rb +309 -0
  27. data/lib/asciichem/formatter/structural_svg.rb +286 -0
  28. data/lib/asciichem/formatter/svg.rb +141 -0
  29. data/lib/asciichem/formatter/text.rb +143 -0
  30. data/lib/asciichem/formatter.rb +43 -0
  31. data/lib/asciichem/grammar.rb +344 -0
  32. data/lib/asciichem/greek.rb +80 -0
  33. data/lib/asciichem/layout.rb +313 -0
  34. data/lib/asciichem/linter/balance_check.rb +103 -0
  35. data/lib/asciichem/linter/base.rb +52 -0
  36. data/lib/asciichem/linter/bracket_balance_check.rb +44 -0
  37. data/lib/asciichem/linter/diagnostic.rb +16 -0
  38. data/lib/asciichem/linter/element_validation_check.rb +39 -0
  39. data/lib/asciichem/linter/isotope_sanity_check.rb +47 -0
  40. data/lib/asciichem/linter/registry.rb +31 -0
  41. data/lib/asciichem/linter/unclosed_ring_check.rb +27 -0
  42. data/lib/asciichem/linter/valence_check.rb +91 -0
  43. data/lib/asciichem/linter.rb +40 -0
  44. data/lib/asciichem/model/atom.rb +73 -0
  45. data/lib/asciichem/model/bond.rb +41 -0
  46. data/lib/asciichem/model/electron_configuration.rb +36 -0
  47. data/lib/asciichem/model/embedded_math.rb +26 -0
  48. data/lib/asciichem/model/formula.rb +31 -0
  49. data/lib/asciichem/model/group.rb +50 -0
  50. data/lib/asciichem/model/identifier.rb +14 -0
  51. data/lib/asciichem/model/molecule.rb +70 -0
  52. data/lib/asciichem/model/name.rb +15 -0
  53. data/lib/asciichem/model/node.rb +100 -0
  54. data/lib/asciichem/model/reaction.rb +49 -0
  55. data/lib/asciichem/model/reaction_cascade.rb +33 -0
  56. data/lib/asciichem/model/text.rb +23 -0
  57. data/lib/asciichem/model.rb +23 -0
  58. data/lib/asciichem/model_adapter/from_canonical.rb +272 -0
  59. data/lib/asciichem/model_adapter/to_canonical.rb +429 -0
  60. data/lib/asciichem/model_adapter.rb +56 -0
  61. data/lib/asciichem/parser.rb +36 -0
  62. data/lib/asciichem/periodic_table.rb +112 -0
  63. data/lib/asciichem/ring_bonds.rb +68 -0
  64. data/lib/asciichem/transform.rb +513 -0
  65. data/lib/asciichem/version.rb +5 -0
  66. data/lib/asciichem/xml_builder.rb +9 -0
  67. data/lib/asciichem.rb +34 -0
  68. metadata +197 -0
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Ribose Inc.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.adoc ADDED
@@ -0,0 +1,55 @@
1
+ = AsciiChem
2
+
3
+ AsciiChem is an ASCII syntax for representing chemistry: atoms, molecules,
4
+ bonds, reactions, electron configurations, and embedded mathematics. It
5
+ parses to a semantic model and renders to MathML, HTML, LaTeX, and SVG.
6
+
7
+ == Why
8
+
9
+ AsciiMath is insufficient for chemistry:
10
+
11
+ * Prefix superscripts/subscripts have no atom to bind to — AsciiMath
12
+ forces a phantom `{}` carrier (e.g. `{}^14C`), which is semantically
13
+ wrong: the isotope belongs to the atom, not to an empty element.
14
+ * No native syntax for stoichiometric coefficients, reaction arrows,
15
+ conditions, or spectator ions.
16
+ * No representation for electron configuration, term symbols, or bonds.
17
+ * No structural diagrams.
18
+
19
+ AsciiChem closes these gaps.
20
+
21
+ == Installation
22
+
23
+ Add to your Gemfile:
24
+
25
+ [source,ruby]
26
+ ----
27
+ gem "asciichem"
28
+ ----
29
+
30
+ Or install directly:
31
+
32
+ [source,sh]
33
+ ----
34
+ $ gem install asciichem
35
+ ----
36
+
37
+ == Usage
38
+
39
+ [source,ruby]
40
+ ----
41
+ require "asciichem"
42
+
43
+ formula = AsciiChem.parse("H_2O")
44
+ formula.to_mathml # => "<math>...</math>"
45
+ formula.to_text # => "H_2O" (round trip)
46
+ ----
47
+
48
+ == Design
49
+
50
+ See link:ARCHITECTURE.adoc[] and the specification at
51
+ https://www.asciichem.org[].
52
+
53
+ == License
54
+
55
+ BSD-2-Clause. See link:LICENSE[LICENSE].
data/RELEASING.md ADDED
@@ -0,0 +1,102 @@
1
+ = Releasing AsciiChem
2
+
3
+ == Versioning
4
+
5
+ AsciiChem follows https://semver.org/[Semantic Versioning]. While the
6
+ public API is still stabilising (0.x), breaking changes bump the minor
7
+ version; additive changes and fixes bump the patch.
8
+
9
+ == Pre-flight
10
+
11
+ 1. Ensure `main` is green on CI.
12
+ 2. Pull latest `main`.
13
+ 3. Create a release branch: `git switch -c release/v0.X.Y`.
14
+
15
+ == Bump version
16
+
17
+ Edit `lib/asciichem/version.rb`:
18
+
19
+ [source,ruby]
20
+ ----
21
+ module AsciiChem
22
+ VERSION = "0.X.Y"
23
+ end
24
+ ----
25
+
26
+ == Update changelog
27
+
28
+ Add a new section at the top of `CHANGELOG.md`:
29
+
30
+ [source,markdown]
31
+ ----
32
+ ## [0.X.Y] — YYYY-MM-DD
33
+
34
+ ### Added
35
+ - ...
36
+
37
+ ### Changed
38
+ - ...
39
+
40
+ ### Fixed
41
+ - ...
42
+ ----
43
+
44
+ Update the `[Unreleased]` link at the bottom of `CHANGELOG.md` to point
45
+ at the new version's comparison on GitHub.
46
+
47
+ == Verify
48
+
49
+ [source,sh]
50
+ ----
51
+ bundle exec rspec # all specs pass
52
+ bundle exec rake build # gem builds cleanly
53
+ gem install pkg/asciichem-0.X.Y.gem # installs locally
54
+ asciichem version # prints 0.X.Y
55
+ ----
56
+
57
+ == Commit
58
+
59
+ [source,sh]
60
+ ----
61
+ git add lib/asciichem/version.rb CHANGELOG.md
62
+ git commit -m "Bump version to 0.X.Y"
63
+ ----
64
+
65
+ Open a PR for the release branch. **Never commit directly to `main`**
66
+ (global rule).
67
+
68
+ == Tag and push (maintainer only)
69
+
70
+ After the release PR merges:
71
+
72
+ [source,sh]
73
+ ----
74
+ git switch main
75
+ git pull
76
+ git tag v0.X.Y
77
+ git push origin v0.X.Y
78
+ ----
79
+
80
+ The tag triggers no automation; it exists as a marker. Tags are
81
+ permanent and visible — only the maintainer decides when and what to
82
+ tag.
83
+
84
+ == Publish to RubyGems
85
+
86
+ [source,sh]
87
+ ----
88
+ gem push pkg/asciichem-0.X.Y.gem
89
+ ----
90
+
91
+ Verify at https://rubygems.org/gems/asciichem.
92
+
93
+ == Site deploy
94
+
95
+ The asciichem.github.io site redeploys automatically on push to `main`.
96
+ If a release adds new syntax, update the spec pages on the site repo
97
+ in a separate PR.
98
+
99
+ == Post-release
100
+
101
+ 1. Bump `[Unreleased]` back to the top of `CHANGELOG.md`.
102
+ 2. Open a PR titled "Post-release housekeeping" with any doc cleanup.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/asciichem.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/asciichem/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "asciichem"
7
+ spec.version = AsciiChem::VERSION
8
+ spec.authors = ["Ribose Inc."]
9
+ spec.email = ["open.source@ribose.com"]
10
+
11
+ spec.summary = "ASCII syntax for chemistry formulae, reactions, and structures."
12
+ spec.description = "AsciiChem is an ASCII syntax for representing chemical formulae, " \
13
+ "reactions, electron configurations, and bonds. It parses to a semantic " \
14
+ "model and renders to MathML, HTML, LaTeX, and SVG. Math embedding uses Plurimath."
15
+ spec.homepage = "https://www.asciichem.org"
16
+ spec.license = "BSD-2-Clause"
17
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/asciichem/asciichem-ruby"
21
+ spec.metadata["changelog_uri"] = "https://github.com/asciichem/asciichem-ruby/blob/main/CHANGELOG.md"
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ f.match(%r{^(test|spec|features)/})
27
+ end
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_dependency "chemicalml", "0.2.0"
34
+ spec.add_dependency "elkrb", "~> 1.0"
35
+ spec.add_dependency "nokogiri", "~> 1.16"
36
+ spec.add_dependency "parslet", "~> 2.0"
37
+ spec.add_dependency "plurimath", "~> 0.8"
38
+ spec.add_dependency "thor", "~> 1.3"
39
+ end
@@ -0,0 +1,49 @@
1
+ # Performance benchmarks
2
+
3
+ Manual benchmarks for AsciiChem. CI does not run them; they exist to
4
+ surface regressions during development.
5
+
6
+ ## Run
7
+
8
+ ```bash
9
+ bundle exec ruby benchmarks/benchmark.rb
10
+ ```
11
+
12
+ If `benchmark-ips` is installed (add to Gemfile dev group), the output
13
+ is iterations-per-second. Otherwise the script falls back to stdlib
14
+ `Benchmark` and reports wall-clock time per call.
15
+
16
+ ## Suites
17
+
18
+ - **parse** — `AsciiChem.parse(source)` alone.
19
+ - **round-trip** — `parse + to_text`.
20
+ - **to_mathml** — `Formula#to_mathml` on a pre-parsed formula.
21
+
22
+ Each suite runs against a fixed set of inputs spanning every construct:
23
+ atoms, molecules, isotopes, charges, groups, reactions, equilibrium
24
+ with conditions, cascades, electron configs, and bonds.
25
+
26
+ ## Baseline numbers
27
+
28
+ Captured on the maintainer's machine (Apple M1, Ruby 3.4). Update this
29
+ table when significant refactors land; investigate regressions of more
30
+ than 2x.
31
+
32
+ | Input | parse (ips) | round-trip (ips) | to_mathml (ips) |
33
+ |---|---|---|---|
34
+ | simple atom (H) | _tbd_ | _tbd_ | _tbd_ |
35
+ | simple molecule (H_2O) | _tbd_ | _tbd_ | _tbd_ |
36
+ | isotope (^14C) | _tbd_ | _tbd_ | _tbd_ |
37
+ | charged atom (Ca^2+) | _tbd_ | _tbd_ | _tbd_ |
38
+ | group (Ca(OH)_2) | _tbd_ | _tbd_ | _tbd_ |
39
+ | reaction (2H_2 + O_2 -> 2H_2O) | _tbd_ | _tbd_ | _tbd_ |
40
+ | equilibrium (Haber) | _tbd_ | _tbd_ | _tbd_ |
41
+ | cascade (A -> B -> C -> D) | _tbd_ | _tbd_ | _tbd_ |
42
+ | electron config | _tbd_ | _tbd_ | _tbd_ |
43
+ | bonds (H-O-H=O#H) | _tbd_ | _tbd_ | _tbd_ |
44
+
45
+ ## When to investigate
46
+
47
+ - Any input that drops below 100 ips on parse.
48
+ - A regression of more than 2x in any cell.
49
+ - New grammar productions that fall off the table's curve.
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Performance benchmarks for AsciiChem. Uses benchmark-ips if available;
5
+ # falls back to stdlib Benchmark otherwise.
6
+ #
7
+ # Run manually:
8
+ # bundle exec ruby benchmarks/benchmark.rb
9
+ #
10
+ # CI does not run benchmarks. They are a manual check for regressions
11
+ # during development.
12
+
13
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
14
+ require "asciichem"
15
+ require "benchmark"
16
+
17
+ begin
18
+ require "benchmark/ips"
19
+ HAVE_IPS = true
20
+ rescue LoadError
21
+ HAVE_IPS = false
22
+ end
23
+
24
+ CASES = {
25
+ "simple atom (H)" => "H",
26
+ "simple molecule (H_2O)" => "H_2O",
27
+ "isotope (^14C)" => "^14C",
28
+ "charged atom (Ca^2+)" => "Ca^2+",
29
+ "group (Ca(OH)_2)" => "Ca(OH)_2",
30
+ "reaction (2H_2 + O_2 -> 2H_2O)" => "2H_2 + O_2 -> 2H_2O",
31
+ "equilibrium (Haber)" => "N_2 + 3H_2 <=>[Fe][400°C] 2NH_3",
32
+ "cascade (A -> B -> C -> D)" => "A -> B -> C -> D",
33
+ "electron config" => "1s^2 2s^2 2p^6 3s^2 3p^6 4s^2 3d^10",
34
+ "bonds (H-O-H=O#H)" => "H-O-H=O#H"
35
+ }.freeze
36
+
37
+ def parse_bench(label, source)
38
+ if HAVE_IPS
39
+ Benchmark.ips do |x|
40
+ x.config(time: 2, warmup: 1)
41
+ x.report("#{label} parse") { AsciiChem.parse(source) }
42
+ end
43
+ else
44
+ Benchmark.realtime { 100.times { AsciiChem.parse(source) } }
45
+ .then { |t| printf " %-40s parse: %6.2f ms/call\n", label, t * 10 }
46
+ end
47
+ end
48
+
49
+ def roundtrip_bench(label, source)
50
+ if HAVE_IPS
51
+ Benchmark.ips do |x|
52
+ x.config(time: 2, warmup: 1)
53
+ x.report("#{label} round-trip") do
54
+ AsciiChem.parse(source).to_text
55
+ end
56
+ end
57
+ else
58
+ Benchmark.realtime { 100.times { AsciiChem.parse(source).to_text } }
59
+ .then { |t| printf " %-40s round-trip: %6.2f ms/call\n", label, t * 10 }
60
+ end
61
+ end
62
+
63
+ def mathml_bench(label, source)
64
+ formula = AsciiChem.parse(source)
65
+ if HAVE_IPS
66
+ Benchmark.ips do |x|
67
+ x.config(time: 2, warmup: 1)
68
+ x.report("#{label} to_mathml") { formula.to_mathml }
69
+ end
70
+ else
71
+ Benchmark.realtime { 100.times { formula.to_mathml } }
72
+ .then { |t| printf " %-40s to_mathml: %6.2f ms/call\n", label, t * 10 }
73
+ end
74
+ end
75
+
76
+ def cml_bench(label, source)
77
+ formula = AsciiChem.parse(source)
78
+ if HAVE_IPS
79
+ Benchmark.ips do |x|
80
+ x.config(time: 2, warmup: 1)
81
+ x.report("#{label} to_cml") { formula.to_cml }
82
+ x.report("#{label} cml parse") { AsciiChem::Cml.parse(formula.to_cml) }
83
+ end
84
+ else
85
+ Benchmark.realtime { 100.times { formula.to_cml } }
86
+ .then { |t| printf " %-40s to_cml: %6.2f ms/call\n", label, t * 10 }
87
+ end
88
+ end
89
+
90
+ puts "AsciiChem #{AsciiChem::VERSION} (benchmark-ips: #{HAVE_IPS})"
91
+ puts
92
+
93
+ puts "== parse =="
94
+ CASES.each { |label, src| parse_bench(label, src) }
95
+ puts
96
+
97
+ puts "== round-trip (parse + to_text) =="
98
+ CASES.each { |label, src| roundtrip_bench(label, src) }
99
+ puts
100
+
101
+ puts "== to_mathml =="
102
+ CASES.each { |label, src| mathml_bench(label, src) }
103
+ puts
104
+
105
+ puts "== CML (to_cml + parse) =="
106
+ CASES.first(5).each { |label, src| cml_bench(label, src) }
data/exe/asciichem ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "asciichem"
5
+
6
+ AsciiChem::Cli.start(ARGV)
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module AsciiChem
6
+ # Thor-based command line interface. Invoked via the `asciichem`
7
+ # executable (exe/asciichem).
8
+ class Cli < Thor
9
+ # Use lowercase 'asciichem' as the program name in help output
10
+ # and command banners, matching the executable name.
11
+ package_name "asciichem"
12
+
13
+ desc "convert -i INPUT -t FORMAT", "Convert AsciiChem INPUT to FORMAT (mathml|text|html|latex|svg|cml)"
14
+ method_option :input, aliases: "-i", type: :string, required: true,
15
+ desc: "AsciiChem source text (or '-' for stdin)"
16
+ method_option :file, aliases: "-f", type: :string,
17
+ desc: "Read AsciiChem source from a file"
18
+ method_option :format, aliases: "-t", type: :string, default: "mathml",
19
+ desc: "Output format"
20
+ def convert
21
+ source = read_source
22
+ formula = AsciiChem.parse(source)
23
+ puts render(formula, options[:format])
24
+ rescue AsciiChem::ParseError => e
25
+ warn "Parse error: #{e.message}"
26
+ exit 1
27
+ rescue AsciiChem::FormatError => e
28
+ warn "Format error: #{e.message}"
29
+ exit 2
30
+ end
31
+
32
+ desc "parse-cml -i INPUT", "Parse CML XML and emit AsciiChem text"
33
+ method_option :input, aliases: "-i", type: :string, required: true,
34
+ desc: "CML XML source"
35
+ def parse_cml
36
+ formula = AsciiChem::Cml.parse(options[:input])
37
+ puts formula.to_text
38
+ rescue AsciiChem::Error => e
39
+ warn "CML parse error: #{e.message}"
40
+ exit 1
41
+ end
42
+
43
+ desc "roundtrip -i INPUT", "Parse and re-emit; exit non-zero if not equal"
44
+ method_option :input, aliases: "-i", type: :string, required: true
45
+ def roundtrip
46
+ original = options[:input]
47
+ rendered = AsciiChem.parse(original).to_text
48
+ if rendered == original
49
+ puts rendered
50
+ exit 0
51
+ else
52
+ warn "round-trip mismatch:\n input: #{original.inspect}\n rendered: #{rendered.inspect}"
53
+ exit 1
54
+ end
55
+ end
56
+
57
+ desc "lint -i INPUT", "Run chemistry checks; exit 1 on error, 0 if clean"
58
+ method_option :input, aliases: "-i", type: :string, required: true,
59
+ desc: "AsciiChem source text"
60
+ def lint
61
+ formula = AsciiChem.parse(options[:input])
62
+ diagnostics = AsciiChem::Linter.run(formula)
63
+ diagnostics.each { |d| puts d }
64
+ exit diagnostics.any? { |d| d.severity == :error } ? 1 : 0
65
+ rescue AsciiChem::ParseError => e
66
+ warn "Parse error: #{e.message}"
67
+ exit 1
68
+ end
69
+
70
+ map %w[--version -v] => :version
71
+ desc "version", "Print the asciichem version"
72
+ def version
73
+ puts "asciichem #{AsciiChem::VERSION}"
74
+ end
75
+
76
+ # Override banner to use lowercase program name consistently.
77
+ def self.banner(command, _namespace = nil, _subcommand = false)
78
+ "asciichem #{command.usage}"
79
+ end
80
+
81
+ private
82
+
83
+ def read_source
84
+ return File.read(options[:file]) if options[:file]
85
+ return $stdin.read if options[:input] == "-"
86
+
87
+ options[:input]
88
+ end
89
+
90
+ def render(formula, format)
91
+ return formula.to_cml if format.to_sym == :cml
92
+
93
+ AsciiChem::Formatter.render(format.to_sym, formula)
94
+ end
95
+ end
96
+ end