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
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
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,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
|