asciichem 0.28.2 → 0.29.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/.github/workflows/ci.yml +9 -5
- data/.github/workflows/release.yml +1 -1
- data/CHANGELOG.md +21 -0
- data/benchmarks/README.md +25 -0
- data/benchmarks/parsanol_recheck.rb +61 -29
- data/lib/asciichem/engine/parsanol_engine.rb +118 -0
- data/lib/asciichem/engine/parslet_engine.rb +34 -0
- data/lib/asciichem/engine.rb +59 -0
- data/lib/asciichem/grammar.rb +2 -457
- data/lib/asciichem/grammar_rules.rb +476 -0
- data/lib/asciichem/parser.rb +8 -10
- data/lib/asciichem/transform.rb +1 -851
- data/lib/asciichem/transform_rules.rb +855 -0
- data/lib/asciichem/version.rb +1 -1
- data/lib/asciichem.rb +3 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 106d03bf9d192f66b4f9ade534ed84f8dcc5cbf0ceeb07d45b633c353fc92aaa
|
|
4
|
+
data.tar.gz: 36c047aa3f26f73f94cc698eb905a939025feb9fc34fb86b67163cf3a93f7fbe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9215fa19d5dc3a65d82cfbe5cdfd4f837bd136cbb18cb0241654e7307a314c42088a870a76c3393858bd285e8e5834a95de136849c6f4d1ee4d2469fe616731
|
|
7
|
+
data.tar.gz: f7654b2d8257c634e7be6b514011b5875c8060886204c43dd9293b982c8cd241773fb92958d8772d0e28ec07ce50ad82753aba085469e0c3edbc03446201c3bc
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -13,13 +13,17 @@ jobs:
|
|
|
13
13
|
matrix:
|
|
14
14
|
ruby: ["3.3", "3.4"]
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
17
|
- name: Clone conformance corpus (asciichem-tests)
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
uses: actions/checkout@v7
|
|
19
|
+
with:
|
|
20
|
+
repository: asciichem/asciichem-tests
|
|
21
|
+
path: .asciichem-tests
|
|
22
|
+
- name: Point the suite at the corpus and record its version
|
|
20
23
|
run: |
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
echo "ASCIICHEM_CORPUS=$(pwd)/.asciichem-tests/corpus/fixtures" >> "$GITHUB_ENV"
|
|
25
|
+
git -C .asciichem-tests fetch --depth 1 --tags --quiet
|
|
26
|
+
echo "ASCIICHEM_CORPUS_VERSION=$(git -C .asciichem-tests describe --tags --abbrev=0 2>/dev/null || echo main)" >> "$GITHUB_ENV"
|
|
23
27
|
- uses: ruby/setup-ruby@v1
|
|
24
28
|
with:
|
|
25
29
|
ruby-version: ${{ matrix.ruby }}
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
All notable changes to AsciiChem are documented here.
|
|
4
4
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.29.0] - 2026-09-15
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Opt-in Parsanol parsing engine (TODO.impl 63/64; parsanol-ruby#25):
|
|
10
|
+
`AsciiChem::Engine.use(:parsanol)` runs the SAME grammar and
|
|
11
|
+
transform (extracted into backend-neutral GrammarRules /
|
|
12
|
+
TransformRules modules) over Parsanol's Rust-backed
|
|
13
|
+
parslet-compat layer - the full suite (1981 examples) passes
|
|
14
|
+
identically under either engine, at ~2.4x parse speed
|
|
15
|
+
(219 vs 90 i/s on the benchmark workload). Parsanol is a soft
|
|
16
|
+
dependency (gemspec unchanged); absent gem raises guidance.
|
|
17
|
+
ASCIICHEM_ENGINE=parsanol selects it for test runs.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Cascade legs are captured as one :segments repeat (the
|
|
21
|
+
electron-config pattern) so every engine arrays them; parsanol
|
|
22
|
+
merges - and overwrites - bare repeated sibling captures,
|
|
23
|
+
silently dropping legs (reported upstream). Transform
|
|
24
|
+
canonicaliser consumes the segments shape; spec'd for scalar and
|
|
25
|
+
array forms.
|
|
26
|
+
|
|
6
27
|
## [0.28.2] - 2026-09-14
|
|
7
28
|
|
|
8
29
|
### Fixed
|
data/benchmarks/README.md
CHANGED
|
@@ -85,3 +85,28 @@ a meaningful re-measure.** Corpus correctness is already there; the
|
|
|
85
85
|
native path is the whole point and remains unmeasurable until
|
|
86
86
|
serialization survives a multi-rule grammar.
|
|
87
87
|
|
|
88
|
+
### Re-check 3 (2026-09-15, parsanol 1.3.15)
|
|
89
|
+
|
|
90
|
+
The mode-routing/VM rework landed; native now engages for the full
|
|
91
|
+
grammar (`PARSANOL_MODE=native` in `benchmarks/parsanol_recheck.rb`,
|
|
92
|
+
fork-per-case gate so Rust aborts are reported, not fatal):
|
|
93
|
+
|
|
94
|
+
- **219/221 corpus cases green under native** — every accept case
|
|
95
|
+
except the two embedded-math inputs, and all 51 rejects clean
|
|
96
|
+
- **3.2x faster than parslet** on the workload (4.28 ms vs 13.64 ms
|
|
97
|
+
per 10-input pass, same session, ±3.0%)
|
|
98
|
+
- The two failures are the embedded-math grammar paths hitting
|
|
99
|
+
`serialize_dynamic` — the still-unfixed `@next_id` collision from
|
|
100
|
+
the re-check above (manifests as the Rust panic or a Ruby-side
|
|
101
|
+
`NoMethodError` on the native error path). Upstream thread:
|
|
102
|
+
parsanol-ruby#25 (third comment).
|
|
103
|
+
- Separately noted upstream: `H2` / `_2O` are accepted under native
|
|
104
|
+
but rejected under parslet (optimizer Str/Re run-merging semantics;
|
|
105
|
+
no corpus case covers these spellings today).
|
|
106
|
+
|
|
107
|
+
**Verdict: one upstream one-liner from adoption evaluation.** With
|
|
108
|
+
`@next_id += 1` fixed, the entire corpus passes under native at
|
|
109
|
+
3x parslet speed — at that point the decision is whether to make the
|
|
110
|
+
engine switchable (opt-in, soft dependency) in the gem.
|
|
111
|
+
|
|
112
|
+
|
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Parsanol re-check (parsanol-ruby 1.3.
|
|
3
|
+
# Parsanol re-check (parsanol-ruby 1.3.15, issue-25 thread): runs the
|
|
4
4
|
# UNMODIFIED AsciiChem grammar over the Parsanol engine via the
|
|
5
5
|
# Parslet compat shim, then (1) gates on the shared corpus, (2) gates
|
|
6
6
|
# on the issue-25 EOF repro, (3) measures against the parslet path.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
8
|
+
# Mode: native by default (PARSOLAN_MODE=native — default routing);
|
|
9
|
+
# PARSANOL_MODE=ruby forces the pure-Ruby engine. As of 1.3.15 the
|
|
10
|
+
# native backend parses 169/170 corpus accepts + all 51 rejects at
|
|
11
|
+
# ~2.3x parslet speed; the single fatal case is embedded-math input,
|
|
12
|
+
# which panics the Rust core on the known @next_id collision
|
|
13
|
+
# (parsanol-ruby#25) — run that one in :ruby until upstream fixes it.
|
|
13
14
|
#
|
|
14
|
-
# Run from asciichem-ruby
|
|
15
|
+
# Run from asciichem-ruby/ with the shim dir prepended:
|
|
15
16
|
# ruby -I /tmp/parsanol_spike -I ../parsanol/parsanol-ruby/lib benchmarks/parsanol_recheck.rb
|
|
16
17
|
require "benchmark/ips"
|
|
17
18
|
require "asciichem"
|
|
18
19
|
require "json"
|
|
19
20
|
|
|
20
|
-
Parsanol::Native.singleton_class.define_method(:available?) { false } # spike: force :ruby
|
|
21
|
-
|
|
22
21
|
puts "parsanol #{Parsanol::VERSION} | parslet-compat Parser=#{Parsanol::Parslet::Parser}"
|
|
23
22
|
puts "AsciiChem::Grammar superclass: #{AsciiChem::Grammar.superclass}"
|
|
23
|
+
puts "mode: #{ENV.fetch("PARSANOL_MODE", "native")}"
|
|
24
|
+
|
|
25
|
+
if ENV.fetch("PARSANOL_MODE", "native") == "ruby"
|
|
26
|
+
Parsanol::Native.singleton_class.define_method(:available?) { false }
|
|
27
|
+
end
|
|
24
28
|
|
|
25
29
|
# -- 1. Issue-25 repro: repeat-of-maybe at end of input --------------
|
|
26
30
|
begin
|
|
@@ -31,38 +35,66 @@ rescue AsciiChem::ParseError => e
|
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
# -- 2. Shared-corpus gate --------------------------------------------
|
|
38
|
+
# Each case runs in a forked child: a Rust panic aborts the child
|
|
39
|
+
# process (unrescuable in Ruby), and the parent reports it by name
|
|
40
|
+
# instead of dying.
|
|
34
41
|
corpus_dir = File.expand_path("../../asciichem-tests/corpus/fixtures", __dir__)
|
|
35
42
|
cases = Dir[File.join(corpus_dir, "*.json")].sort.flat_map { |p| JSON.parse(File.read(p)) }
|
|
36
43
|
parser_cases = cases.select { |c| c.key?("input") && !c.key?("lint") && !c.key?("convention") }
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
def run_in_child
|
|
46
|
+
reader, writer = IO.pipe
|
|
47
|
+
pid = fork do
|
|
48
|
+
reader.close
|
|
49
|
+
Marshal.dump(yield, writer)
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
Marshal.dump({ exception: e.class.name, message: e.message }, writer)
|
|
52
|
+
ensure
|
|
53
|
+
writer.close
|
|
54
|
+
end
|
|
55
|
+
writer.close
|
|
56
|
+
payload = Marshal.load(reader)
|
|
57
|
+
reader.close
|
|
58
|
+
_, status = Process.waitpid2(pid)
|
|
59
|
+
[payload, status]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
pass = fail_parse = fail_reject = fail_roundtrip = fatal = 0
|
|
39
63
|
parser_cases.each do |fixture|
|
|
40
64
|
input = fixture.fetch("input")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
payload, status = run_in_child do
|
|
66
|
+
formula = AsciiChem.parse(input)
|
|
67
|
+
{ text: (formula.to_text if fixture["roundTrip"]) }
|
|
68
|
+
end
|
|
69
|
+
if status.signaled? || !status.success?
|
|
70
|
+
fatal += 1
|
|
71
|
+
puts " FATAL (child #{status.exitstatus ? "exit #{status.exitstatus}" : "aborted"}): #{fixture["id"]} #{input.inspect}" if fatal <= 8
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
if payload.key?(:exception)
|
|
75
|
+
if fixture.fetch("parses")
|
|
50
76
|
fail_parse += 1
|
|
51
|
-
puts " PARSE FAIL: #{input.inspect} -> #{
|
|
52
|
-
|
|
53
|
-
else
|
|
54
|
-
begin
|
|
55
|
-
AsciiChem.parse(input)
|
|
56
|
-
fail_reject += 1
|
|
57
|
-
puts " SHOULD REJECT: #{input.inspect}" if fail_reject <= 8
|
|
58
|
-
rescue AsciiChem::ParseError, Parslet::ParseFailed
|
|
77
|
+
puts " PARSE FAIL: #{input.inspect} -> #{payload[:message][0, 90]}" if fail_parse <= 8
|
|
78
|
+
else
|
|
59
79
|
pass += 1
|
|
60
80
|
end
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
unless fixture.fetch("parses")
|
|
84
|
+
fail_reject += 1
|
|
85
|
+
puts " SHOULD REJECT: #{input.inspect}" if fail_reject <= 8
|
|
86
|
+
next
|
|
87
|
+
end
|
|
88
|
+
if fixture["roundTrip"] && payload[:text] != input
|
|
89
|
+
fail_roundtrip += 1
|
|
90
|
+
puts " ROUNDTRIP DIFF: #{input.inspect} -> #{payload[:text].inspect}" if fail_roundtrip <= 5
|
|
91
|
+
next
|
|
61
92
|
end
|
|
93
|
+
pass += 1
|
|
62
94
|
end
|
|
63
95
|
total = parser_cases.length
|
|
64
|
-
puts format("corpus gate: %d/%d ok (parse-fails %d, should-reject %d, roundtrip-diffs %d)",
|
|
65
|
-
pass, total, fail_parse, fail_reject, fail_roundtrip)
|
|
96
|
+
puts format("corpus gate: %d/%d ok (parse-fails %d, should-reject %d, roundtrip-diffs %d, fatal %d)",
|
|
97
|
+
pass, total, fail_parse, fail_reject, fail_roundtrip, fatal)
|
|
66
98
|
|
|
67
99
|
# -- 3. Performance ----------------------------------------------------
|
|
68
100
|
WORKLOAD = [
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Opt-in soft dependency: fail with guidance, never silently.
|
|
4
|
+
begin
|
|
5
|
+
require 'parsanol/parslet'
|
|
6
|
+
# parsanol-ruby 1.3.16's native.rb references
|
|
7
|
+
# Parsanol::Native::Dynamic from the serializer without loading it
|
|
8
|
+
# (parsanol-ruby#25).
|
|
9
|
+
require 'parsanol/native/dynamic'
|
|
10
|
+
rescue LoadError => e
|
|
11
|
+
raise AsciiChem::Engine::NotAvailableError,
|
|
12
|
+
'the parsanol engine requires the parsanol gem — add gem "parsanol", "~> 1.3.16" ' \
|
|
13
|
+
"to your Gemfile (#{e.message})"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module AsciiChem
|
|
17
|
+
module Engine
|
|
18
|
+
# The Parsanol engine: the SAME rules (GrammarRules /
|
|
19
|
+
# TransformRules) over Parsanol's Rust-backed parslet-compat
|
|
20
|
+
# layer. Corpus-identical to :parslet (221/221 byte-for-byte,
|
|
21
|
+
# benchmarks/parsanol_recheck.rb) at ~3x parse speed.
|
|
22
|
+
class ParsanolEngine
|
|
23
|
+
# Backend twins of the reference classes; the shared rules
|
|
24
|
+
# modules are the single source of grammar truth.
|
|
25
|
+
class Grammar < ::Parsanol::Parslet::Parser
|
|
26
|
+
include AsciiChem::GrammarRules
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Transform twin; #apply normalizes the engine's merged sibling
|
|
30
|
+
# captures before the shared rules run (see split_merged_formula).
|
|
31
|
+
class Transform < ::Parsanol::Parslet::Transform
|
|
32
|
+
include AsciiChem::TransformRules
|
|
33
|
+
|
|
34
|
+
# Engine divergence seam: parslet yields repeated sibling
|
|
35
|
+
# node captures as an array of single-key hashes; parsanol
|
|
36
|
+
# merges them into one hash, which no rule can match. Split
|
|
37
|
+
# the merged form back before the shared rules run, so the
|
|
38
|
+
# rules stay engine-agnostic (single source of truth).
|
|
39
|
+
def apply(tree, context = nil)
|
|
40
|
+
super(ParsanolEngine.split_merged_formula(tree), context)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
# NODE_STARTERS mirrors the grammar's `node` alternatives
|
|
46
|
+
# (grammar_rules.rb): the capture key each alternative
|
|
47
|
+
# produces FIRST. A new node group begins at each starter;
|
|
48
|
+
# continuation keys (arrow/products, coefficient/annotations,
|
|
49
|
+
# crystal_name/params/body, ...) attach to the current group.
|
|
50
|
+
NODE_STARTERS = %i[
|
|
51
|
+
cascade reactants electron_config crystal_node spectrum_node
|
|
52
|
+
calc_node zmatrix_node mechanism_node mol units math_source text_run
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
# Grammar truth: a molecule node is {coefficient? stereo? units}
|
|
56
|
+
# — coefficient/stereo may LEGITIMATELY precede :units in one
|
|
57
|
+
# node. They are prefixes, not new-node markers.
|
|
58
|
+
PREFIX_KEYS = %i[coefficient stereo].freeze
|
|
59
|
+
|
|
60
|
+
def split_merged_formula(node)
|
|
61
|
+
case node
|
|
62
|
+
when Hash
|
|
63
|
+
node.to_h { |key, value| [key, split_formula_value(key, value)] }
|
|
64
|
+
when Array
|
|
65
|
+
node.map { |member| split_merged_formula(member) }
|
|
66
|
+
else
|
|
67
|
+
node
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def grammar
|
|
72
|
+
Grammar
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def transform
|
|
76
|
+
Transform
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_failed
|
|
80
|
+
::Parsanol::ParseFailed
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def grammar_instance
|
|
84
|
+
@grammar_instance ||= grammar.new
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def transform_instance
|
|
88
|
+
@transform_instance ||= transform.new
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def split_formula_value(key, value)
|
|
94
|
+
if key == :formula && value.is_a?(Hash) && value.length > 1
|
|
95
|
+
groups = []
|
|
96
|
+
value.each do |k, v|
|
|
97
|
+
groups << {} if new_group?(groups, k)
|
|
98
|
+
groups.last[k] = split_merged_formula(v)
|
|
99
|
+
end
|
|
100
|
+
groups.length == 1 ? groups.first : groups
|
|
101
|
+
else
|
|
102
|
+
split_merged_formula(value)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def new_group?(groups, key)
|
|
107
|
+
return true if groups.empty?
|
|
108
|
+
|
|
109
|
+
return false unless NODE_STARTERS.include?(key)
|
|
110
|
+
|
|
111
|
+
# :units continues a group that so far holds only molecule
|
|
112
|
+
# prefixes ({coefficient:...} / {stereo:...}).
|
|
113
|
+
!(key == :units && groups.last.keys.all? { |pk| PREFIX_KEYS.include?(pk) })
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
module Engine
|
|
5
|
+
# The reference engine: pure-Ruby parslet. Zero extra
|
|
6
|
+
# dependencies; Grammar and Transform as always.
|
|
7
|
+
class ParsletEngine
|
|
8
|
+
class << self
|
|
9
|
+
def grammar
|
|
10
|
+
Grammar
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def transform
|
|
14
|
+
Transform
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse_failed
|
|
18
|
+
Parslet::ParseFailed
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Engines memoize their grammar/transform instances — they are
|
|
22
|
+
# stateless and expensive to construct (benchmark: ~15%
|
|
23
|
+
# throughput on repeated parses).
|
|
24
|
+
def grammar_instance
|
|
25
|
+
@grammar_instance ||= grammar.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def transform_instance
|
|
29
|
+
@transform_instance ||= transform.new
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciiChem
|
|
4
|
+
# Parsing engine selector (TODO.impl 63/64; parsanol-ruby#25).
|
|
5
|
+
#
|
|
6
|
+
# The reference engine is :parslet (pure Ruby, zero extra
|
|
7
|
+
# dependencies) and remains the default. The :parsanol engine runs
|
|
8
|
+
# the SAME rules (GrammarRules/TransformRules) over Parsanol's
|
|
9
|
+
# Rust-backed parslet-compat layer — the shared corpus passes
|
|
10
|
+
# byte-for-byte (221/221) at ~3x parslet speed.
|
|
11
|
+
#
|
|
12
|
+
# Parsanol is an opt-in soft dependency; the gemspec is unchanged:
|
|
13
|
+
#
|
|
14
|
+
# # Gemfile
|
|
15
|
+
# gem "parsanol", "~> 1.3.16"
|
|
16
|
+
#
|
|
17
|
+
# require "asciichem/engine/parsanol_engine"
|
|
18
|
+
# AsciiChem::Engine.use(:parsanol)
|
|
19
|
+
# AsciiChem.parse("H_2O") # parsed by the Rust core
|
|
20
|
+
#
|
|
21
|
+
# The active engine may also be selected via ASCIICHEM_ENGINE
|
|
22
|
+
# (a testing seam, mirroring ASCIICHEM_CORPUS):
|
|
23
|
+
#
|
|
24
|
+
# ASCIICHEM_ENGINE=parsanol bundle exec rspec
|
|
25
|
+
#
|
|
26
|
+
module Engine
|
|
27
|
+
class Error < AsciiChem::Error; end
|
|
28
|
+
|
|
29
|
+
# The named engine exists but its gem is not installed.
|
|
30
|
+
class NotAvailableError < Error; end
|
|
31
|
+
|
|
32
|
+
autoload :ParsletEngine, 'asciichem/engine/parslet_engine'
|
|
33
|
+
autoload :ParsanolEngine, 'asciichem/engine/parsanol_engine'
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Selects the engine by name. Returns the engine class.
|
|
37
|
+
def use(name)
|
|
38
|
+
@current = engine_for(name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The active engine. Defaults to :parslet, overridable once via
|
|
42
|
+
# ASCIICHEM_ENGINE at first use.
|
|
43
|
+
def current
|
|
44
|
+
@current ||= engine_for(ENV.fetch('ASCIICHEM_ENGINE', 'parslet'))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def engine_for(name)
|
|
50
|
+
case name.to_s
|
|
51
|
+
when 'parslet' then ParsletEngine
|
|
52
|
+
when 'parsanol' then ParsanolEngine
|
|
53
|
+
else
|
|
54
|
+
raise Error, "unknown engine #{name.inspect} (available: parslet, parsanol)"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|