asciichem 0.31.15 → 0.31.16
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 +13 -0
- data/asciichem.gemspec +1 -1
- data/benchmarks/RESULTS.md +26 -0
- data/lib/asciichem/cli.rb +0 -15
- data/lib/asciichem/engine.rb +8 -50
- data/lib/asciichem/grammar_rules.rb +2 -2
- data/lib/asciichem/parser.rb +5 -5
- data/lib/asciichem/transform_rules.rb +1 -1
- data/lib/asciichem/version.rb +1 -1
- data/lib/asciichem.rb +0 -1
- metadata +10 -8
- data/lib/asciichem/engine/parsanol_engine.rb +0 -118
- data/lib/asciichem/engine/parslet_engine.rb +0 -34
- data/lib/asciichem/grammar.rb +0 -22
- data/lib/asciichem/transform.rb +0 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b985064f84599e157131e30ca2ae73555383b6e273c6ea3ae561f02d52df5da
|
|
4
|
+
data.tar.gz: bb08895e055a18b16fe280b30da829666bf9f646d269f9a3b5312b61fbf86578
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12604d14213ae4bc6beba2507b6fcd27e540b32d3be966cbdc56816c844f73309e5b6518ea632441e17b7610d7321fcaf25bc2be6ba46659d2165907039c5375
|
|
7
|
+
data.tar.gz: 2676f58b2d881b8a9f321bebcaad712e165f2cc4520f419bdf7b41dab28e4330b72f278456265bd62e3c0101d953501d396435fec84c5281d1379de34637a938
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
All notable changes to AsciiChem are documented here.
|
|
4
4
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.31.16] - 2026-11-12
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- **The legacy parse engines are deleted.** The PARG artifact engine
|
|
10
|
+
is the one engine: `AsciiChem.parse` goes straight through the
|
|
11
|
+
checksummed asciichem PARG artifact (asciichem-parg) — the same
|
|
12
|
+
artifact the Rust crate embeds. Removed: the parslet Parser, the
|
|
13
|
+
parslet Transform wrapper, ParsletEngine, ParsanolEngine, the
|
|
14
|
+
Engine selector + the ASCIICHEM_ENGINE seam, the `--engine` CLI
|
|
15
|
+
flag, and the parslet gem dependency. The shared rule definitions
|
|
16
|
+
the PARG transform consumes stay. 376 lines removed; suite 2327/0.
|
|
17
|
+
- The gemspec carries parsanol (~> 1.3) — the PARG runtime.
|
|
18
|
+
|
|
6
19
|
## [0.31.15] - 2026-11-12
|
|
7
20
|
|
|
8
21
|
### Fixed
|
data/asciichem.gemspec
CHANGED
|
@@ -40,7 +40,7 @@ Gem::Specification.new do |spec|
|
|
|
40
40
|
spec.add_dependency "lutaml-model", "~> 0.8"
|
|
41
41
|
spec.add_dependency "mml", "~> 2.3"
|
|
42
42
|
spec.add_dependency "moxml", "~> 0.6.1"
|
|
43
|
-
spec.add_dependency "
|
|
43
|
+
spec.add_dependency "parsanol", "~> 1.3", ">= 1.3.107"
|
|
44
44
|
spec.add_dependency "relaton-bib", "~> 2.1"
|
|
45
45
|
spec.add_dependency "plurimath", "~> 0.11"
|
|
46
46
|
spec.add_dependency "thor", "~> 1.3"
|
data/benchmarks/RESULTS.md
CHANGED
|
@@ -42,6 +42,32 @@ than 2x.
|
|
|
42
42
|
| electron config | _tbd_ | _tbd_ | _tbd_ |
|
|
43
43
|
| bonds (H-O-H=O#H) | _tbd_ | _tbd_ | _tbd_ |
|
|
44
44
|
|
|
45
|
+
## Cross-runtime parse comparison (2026-11-12)
|
|
46
|
+
|
|
47
|
+
The shared workload (the ten inputs the TS bench and the rs bench
|
|
48
|
+
pin — see `benchmarks/README.md`), per parse input, on the
|
|
49
|
+
maintainer's machine:
|
|
50
|
+
|
|
51
|
+
| Runtime | Engine | per parse |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| TypeScript (peggy) | peggy-generated JS | **~32 µs** |
|
|
54
|
+
| Python (rd) | rd grammar port | ~672 µs |
|
|
55
|
+
| Ruby (parslet) | parslet | ~2,960 µs |
|
|
56
|
+
| Rust (parsanol) | parsanol portable parser | ~200 µs |
|
|
57
|
+
|
|
58
|
+
Recorded via `npm run bench` (asciichem-ts), `python3
|
|
59
|
+
benchmarks/engines.py` (asciichem-py), `bundle exec ruby
|
|
60
|
+
benchmarks/engines.rb` (the gem), and `cargo bench -- --quick`
|
|
61
|
+
(asciichem-rs, criterion, shared workload — see the crate's
|
|
62
|
+
`benches/parse.rs`).
|
|
63
|
+
|
|
64
|
+
Two Rust facts from the same pass: the deep-stack parse thread is
|
|
65
|
+
spawned once and reused, and inputs ≤ 16 KiB parse inline (PEG
|
|
66
|
+
depth is bounded by input length) — both landed in
|
|
67
|
+
asciichem-rs#24. The remaining Rust-vs-peggy gap is parsanol's
|
|
68
|
+
evaluation loop (setup 12 µs, eval ~37 µs for tiny inputs); the
|
|
69
|
+
optimization lever lives in the parsanol repo.
|
|
70
|
+
|
|
45
71
|
## When to investigate
|
|
46
72
|
|
|
47
73
|
- Any input that drops below 100 ips on parse.
|
data/lib/asciichem/cli.rb
CHANGED
|
@@ -20,18 +20,12 @@ module AsciiChem
|
|
|
20
20
|
desc: 'Input grammar: asciichem|smiles|molfile'
|
|
21
21
|
method_option :format, aliases: '-t', type: :string, default: 'mathml',
|
|
22
22
|
desc: 'Output format'
|
|
23
|
-
method_option :engine, type: :string, default: 'parslet',
|
|
24
|
-
desc: 'Parsing engine: parslet (default) | parsanol (opt-in, needs the parsanol gem)'
|
|
25
23
|
def convert
|
|
26
24
|
raise AsciiChem::ParseError, 'provide -i INPUT or -f FILE' unless options['input'] || options['file']
|
|
27
25
|
|
|
28
|
-
select_engine(options[:engine])
|
|
29
26
|
source = read_source
|
|
30
27
|
formula = ingest(source, options[:from])
|
|
31
28
|
puts render(formula, options[:format])
|
|
32
|
-
rescue AsciiChem::Engine::Error => e
|
|
33
|
-
warn "Engine error: #{e.message}"
|
|
34
|
-
exit 6
|
|
35
29
|
rescue AsciiChem::ParseError => e
|
|
36
30
|
warn "Parse error: #{e.message}"
|
|
37
31
|
exit 1
|
|
@@ -206,15 +200,6 @@ module AsciiChem
|
|
|
206
200
|
|
|
207
201
|
private
|
|
208
202
|
|
|
209
|
-
# --engine feeds the Engine selector (0.29.0+); :parsanol is an
|
|
210
|
-
# opt-in soft dependency, :parslet stays the default.
|
|
211
|
-
def select_engine(name)
|
|
212
|
-
return if name.to_s == 'parslet' && AsciiChem::Engine.current == AsciiChem::Engine::ParsletEngine
|
|
213
|
-
|
|
214
|
-
require 'asciichem/engine'
|
|
215
|
-
AsciiChem::Engine.use(name.to_s)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
203
|
def read_source
|
|
219
204
|
return File.read(options[:file]) if options[:file]
|
|
220
205
|
return $stdin.read if options[:input] == '-'
|
data/lib/asciichem/engine.rb
CHANGED
|
@@ -1,61 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AsciiChem
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
-
#
|
|
4
|
+
# The parse engine: the PARG artifact engine, period (TODO.v2/17
|
|
5
|
+
# slice 9; the maintainer's PARG-only directive). The legacy
|
|
6
|
+
# parslet and DSL-parsanol engines are deleted — the checksummed
|
|
7
|
+
# AsciiChem PARG artifact (vendored at engine/parg/asciichem.json,
|
|
8
|
+
# sourced from asciichem-parg) is the one grammar, and
|
|
9
|
+
# PargEngine adapts its tree to the DSL shapes TransformRules
|
|
10
|
+
# consumes.
|
|
26
11
|
module Engine
|
|
27
12
|
class Error < AsciiChem::Error; end
|
|
28
13
|
|
|
29
|
-
# The
|
|
14
|
+
# The engine is the PARG runtime; its gem must be installed.
|
|
30
15
|
class NotAvailableError < Error; end
|
|
31
16
|
|
|
32
|
-
autoload :ParsletEngine, 'asciichem/engine/parslet_engine'
|
|
33
|
-
autoload :ParsanolEngine, 'asciichem/engine/parsanol_engine'
|
|
34
17
|
autoload :PargEngine, 'asciichem/engine/parg_engine'
|
|
35
|
-
|
|
36
|
-
class << self
|
|
37
|
-
# Selects the engine by name. Returns the engine class.
|
|
38
|
-
def use(name)
|
|
39
|
-
@current = engine_for(name)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# The active engine. Defaults to :parslet, overridable once via
|
|
43
|
-
# ASCIICHEM_ENGINE at first use.
|
|
44
|
-
def current
|
|
45
|
-
@current ||= engine_for(ENV.fetch('ASCIICHEM_ENGINE', 'parslet'))
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def engine_for(name)
|
|
51
|
-
case name.to_s
|
|
52
|
-
when 'parslet' then ParsletEngine
|
|
53
|
-
when 'parsanol' then ParsanolEngine
|
|
54
|
-
when 'parg' then PargEngine
|
|
55
|
-
else
|
|
56
|
-
raise Error, "unknown engine #{name.inspect} (available: parslet, parsanol, parg)"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
18
|
end
|
|
61
19
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module AsciiChem
|
|
4
4
|
# The grammar's rules, backend-neutral: included by Grammar
|
|
5
5
|
# (parslet, the reference engine) and by
|
|
6
|
-
#
|
|
6
|
+
# the PARG engine (the one engine, per TODO.v2/17 slice 9)
|
|
7
7
|
# engine is active. The DSL (str, match, any, .repeat, .maybe,
|
|
8
8
|
# .absent?, >>, .as) is identical across backends.
|
|
9
9
|
module GrammarRules
|
|
@@ -164,7 +164,7 @@ module AsciiChem
|
|
|
164
164
|
rule(:reaction_cascade) do
|
|
165
165
|
# The repeat is wrapped in a single :segment capture so every
|
|
166
166
|
# engine arrays it (parsanol merges — and overwrites — bare
|
|
167
|
-
# repeated sibling captures; see
|
|
167
|
+
# repeated sibling captures; see the PARG engine).
|
|
168
168
|
(reaction.as(:first) >>
|
|
169
169
|
(arrow.as(:arrow) >> spaces? >> terms.as(:products)).repeat(1).as(:segments)).as(:cascade)
|
|
170
170
|
end
|
data/lib/asciichem/parser.rb
CHANGED
|
@@ -8,12 +8,12 @@ module AsciiChem
|
|
|
8
8
|
@text = text.to_s
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
11
|
+
# The parse goes through the PARG engine (the leptris-family
|
|
12
|
+
# discipline applies to parsing too: ONE engine, the PARG
|
|
13
|
+
# artifact). Its grammar/transform instances are memoized at the
|
|
14
|
+
# class level.
|
|
15
15
|
def parse
|
|
16
|
-
engine = AsciiChem::Engine
|
|
16
|
+
engine = AsciiChem::Engine::PargEngine
|
|
17
17
|
tree = engine.grammar_instance.parse(text)
|
|
18
18
|
engine.transform_instance.apply(tree)
|
|
19
19
|
rescue engine.parse_failed => e
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module AsciiChem
|
|
4
4
|
# The transform's rules, backend-neutral: included by Transform
|
|
5
|
-
#
|
|
5
|
+
# and by the PARG engine's Transform (the one engine,
|
|
6
6
|
# opt-in engine is active. String class_eval so the nested builder
|
|
7
7
|
# classes land as constants of the including class, unchanged.
|
|
8
8
|
module TransformRules
|
data/lib/asciichem/version.rb
CHANGED
data/lib/asciichem.rb
CHANGED
|
@@ -39,7 +39,6 @@ module AsciiChem
|
|
|
39
39
|
autoload :Rxn, "asciichem/rxn"
|
|
40
40
|
autoload :Smiles, "asciichem/smiles"
|
|
41
41
|
autoload :Structure, "asciichem/structure"
|
|
42
|
-
autoload :Transform, "asciichem/transform"
|
|
43
42
|
autoload :TransformRules, "asciichem/transform_rules"
|
|
44
43
|
autoload :VERSION, "asciichem/version"
|
|
45
44
|
autoload :Wire, "asciichem/wire"
|
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.31.
|
|
4
|
+
version: 0.31.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -80,19 +80,25 @@ dependencies:
|
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: 0.6.1
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
|
-
name:
|
|
83
|
+
name: parsanol
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
85
85
|
requirements:
|
|
86
86
|
- - "~>"
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '
|
|
88
|
+
version: '1.3'
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: 1.3.107
|
|
89
92
|
type: :runtime
|
|
90
93
|
prerelease: false
|
|
91
94
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
95
|
requirements:
|
|
93
96
|
- - "~>"
|
|
94
97
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '
|
|
98
|
+
version: '1.3'
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: 1.3.107
|
|
96
102
|
- !ruby/object:Gem::Dependency
|
|
97
103
|
name: relaton-bib
|
|
98
104
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -196,8 +202,6 @@ files:
|
|
|
196
202
|
- lib/asciichem/engine.rb
|
|
197
203
|
- lib/asciichem/engine/parg/asciichem.json
|
|
198
204
|
- lib/asciichem/engine/parg_engine.rb
|
|
199
|
-
- lib/asciichem/engine/parsanol_engine.rb
|
|
200
|
-
- lib/asciichem/engine/parslet_engine.rb
|
|
201
205
|
- lib/asciichem/errors.rb
|
|
202
206
|
- lib/asciichem/formatter.rb
|
|
203
207
|
- lib/asciichem/formatter/base.rb
|
|
@@ -207,7 +211,6 @@ files:
|
|
|
207
211
|
- lib/asciichem/formatter/structural_svg.rb
|
|
208
212
|
- lib/asciichem/formatter/svg.rb
|
|
209
213
|
- lib/asciichem/formatter/text.rb
|
|
210
|
-
- lib/asciichem/grammar.rb
|
|
211
214
|
- lib/asciichem/grammar_rules.rb
|
|
212
215
|
- lib/asciichem/greek.rb
|
|
213
216
|
- lib/asciichem/identifiers.rb
|
|
@@ -289,7 +292,6 @@ files:
|
|
|
289
292
|
- lib/asciichem/structure.rb
|
|
290
293
|
- lib/asciichem/structure/graph.rb
|
|
291
294
|
- lib/asciichem/structure/linearizer.rb
|
|
292
|
-
- lib/asciichem/transform.rb
|
|
293
295
|
- lib/asciichem/transform_rules.rb
|
|
294
296
|
- lib/asciichem/version.rb
|
|
295
297
|
- lib/asciichem/wire.rb
|
|
@@ -1,118 +0,0 @@
|
|
|
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
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
data/lib/asciichem/grammar.rb
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'parslet'
|
|
4
|
-
|
|
5
|
-
module AsciiChem
|
|
6
|
-
# Parslet grammar for AsciiChem v1.
|
|
7
|
-
#
|
|
8
|
-
# Design notes:
|
|
9
|
-
#
|
|
10
|
-
# - Leaf rules return strings; `.as(:name)` is applied at the
|
|
11
|
-
# combination site so the parse tree is a flat hash of named strings
|
|
12
|
-
# wherever practical.
|
|
13
|
-
# - The prefix-isotope binding (`^14C`) is enforced structurally: the
|
|
14
|
-
# `prefixed_atom` production consumes `^digits element` as a unit.
|
|
15
|
-
# A bare `^digits` without an element is rejected.
|
|
16
|
-
# - Marker rules (`subscript_marker`, `superscript_marker`) capture
|
|
17
|
-
# ONLY the value, not the leading `_` or `^`. The literal prefix is
|
|
18
|
-
# consumed by the rule but not included in the named capture.
|
|
19
|
-
class Grammar < Parslet::Parser
|
|
20
|
-
include GrammarRules
|
|
21
|
-
end
|
|
22
|
-
end
|
data/lib/asciichem/transform.rb
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'parslet'
|
|
4
|
-
|
|
5
|
-
module AsciiChem
|
|
6
|
-
# Converts a parse tree (from AsciiChem::Grammar) into a tree of
|
|
7
|
-
# AsciiChem::Model instances.
|
|
8
|
-
#
|
|
9
|
-
# The transform contains minimal logic — it maps hash shapes to
|
|
10
|
-
# constructor calls. Anything semantically tricky (e.g. distinguishing
|
|
11
|
-
# isotope prefix from suffix charge) is encoded in the grammar, not
|
|
12
|
-
# here.
|
|
13
|
-
class Transform < Parslet::Transform
|
|
14
|
-
include TransformRules
|
|
15
|
-
end
|
|
16
|
-
end
|