asciichem 0.5.1 → 0.19.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +52 -0
  3. data/ARCHITECTURE.adoc +153 -0
  4. data/CHANGELOG.md +363 -5
  5. data/asciichem.gemspec +7 -4
  6. data/benchmarks/benchmark.rb +6 -1
  7. data/lib/asciichem/cli.rb +23 -1
  8. data/lib/asciichem/cml/conditions_extensions.rb +132 -0
  9. data/lib/asciichem/cml/extensions/atom_attributes.rb +192 -0
  10. data/lib/asciichem/cml/extensions/top_level.rb +186 -0
  11. data/lib/asciichem/cml/extensions.rb +32 -344
  12. data/lib/asciichem/cml/group_extensions.rb +10 -20
  13. data/lib/asciichem/cml/metadata_extensions.rb +145 -0
  14. data/lib/asciichem/cml/opaque_extensions.rb +144 -0
  15. data/lib/asciichem/cml/translator.rb +46 -144
  16. data/lib/asciichem/cml.rb +14 -0
  17. data/lib/asciichem/formatter/html.rb +117 -0
  18. data/lib/asciichem/formatter/latex.rb +117 -0
  19. data/lib/asciichem/formatter/mathml.rb +134 -0
  20. data/lib/asciichem/formatter/structural_svg.rb +38 -0
  21. data/lib/asciichem/formatter/svg.rb +38 -6
  22. data/lib/asciichem/formatter/text.rb +14 -10
  23. data/lib/asciichem/grammar.rb +27 -1
  24. data/lib/asciichem/identifiers/base.rb +25 -0
  25. data/lib/asciichem/identifiers/cas.rb +56 -0
  26. data/lib/asciichem/identifiers/inchi.rb +104 -0
  27. data/lib/asciichem/identifiers/inchikey.rb +33 -0
  28. data/lib/asciichem/identifiers/smiles.rb +147 -0
  29. data/lib/asciichem/identifiers.rb +66 -0
  30. data/lib/asciichem/linter/charge_balance_check.rb +117 -0
  31. data/lib/asciichem/linter/crystal_sanity_check.rb +90 -0
  32. data/lib/asciichem/linter/identifier_consistency_check.rb +100 -0
  33. data/lib/asciichem/linter/identifier_format_check.rb +48 -0
  34. data/lib/asciichem/linter/spectrum_peak_check.rb +72 -0
  35. data/lib/asciichem/linter/zmatrix_reference_check.rb +89 -0
  36. data/lib/asciichem/linter.rb +13 -2
  37. data/lib/asciichem/model/atom.rb +31 -0
  38. data/lib/asciichem/model/bond.rb +22 -0
  39. data/lib/asciichem/model/calculation.rb +4 -0
  40. data/lib/asciichem/model/crystal.rb +35 -6
  41. data/lib/asciichem/model/group.rb +30 -12
  42. data/lib/asciichem/model/mechanism.rb +3 -0
  43. data/lib/asciichem/model/molecule.rb +134 -0
  44. data/lib/asciichem/model/node.rb +8 -0
  45. data/lib/asciichem/model/opaque_cml.rb +34 -0
  46. data/lib/asciichem/model/reaction.rb +19 -4
  47. data/lib/asciichem/model/spectrum.rb +5 -0
  48. data/lib/asciichem/model.rb +1 -0
  49. data/lib/asciichem/model_adapter/from_canonical.rb +139 -24
  50. data/lib/asciichem/model_adapter/to_canonical.rb +211 -28
  51. data/lib/asciichem/parser.rb +57 -5
  52. data/lib/asciichem/periodic_table.rb +30 -1
  53. data/lib/asciichem/transform.rb +76 -47
  54. data/lib/asciichem/version.rb +1 -1
  55. data/lib/asciichem.rb +1 -0
  56. metadata +28 -12
  57. data/TODO.beyond-formulas/01-crystallography.md +0 -69
  58. data/TODO.beyond-formulas/02-spectroscopy.md +0 -39
  59. data/TODO.beyond-formulas/03-compchem.md +0 -31
  60. data/TODO.beyond-formulas/04-structural.md +0 -48
  61. data/TODO.beyond-formulas/05-mechanisms.md +0 -40
  62. data/TODO.beyond-formulas/README.md +0 -69
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1edf476bb3d979641172de761d1b2ac906cb4eaf8565e56b0e083e146c09eb1d
4
- data.tar.gz: b3394a0709a475df222d6369eac92a1cc6d02fc756a7d3a45ca038c5d6f5155c
3
+ metadata.gz: 49f3af36729edc3293bb97099d74298533a509ae26aa3c14151a7cef743f654f
4
+ data.tar.gz: 24f5d0778e9f7dce477423b27b1ec5ebcab08472452389f126473234e8500a19
5
5
  SHA512:
6
- metadata.gz: 5a1ce9a990e507a6f70c91939841766e228b02ae674af39c5fa356b4911f20d0ef3b5aa2f2ec4799663d21da54752ed9b68c6660db23e1b2da9cacd4cc50b2ad
7
- data.tar.gz: 6d68778aca7042fce958026033e990e51b87290c24ed3028cf224aa34bd5e03418ef6209b72072d06a14ab3352e2d20290c939b4002a82fdcdbe5c267a17d087
6
+ metadata.gz: 599beaeb33d3bf776076632c83d2c92c16a08571088f3bb272163f61a9082e250af150b3300a80821fc3bb6ab5ffa8d65cc8b5007b4860f0dace0a94b70aaf83
7
+ data.tar.gz: 6fbc557626b9767110bc142c54ae6858b644d563af73f431bd48d8dbd7dabdfeeb79b7ecbb498474d791b35f1717b9ae28689ee1630f352fa48a3e0abcf966a6
@@ -0,0 +1,52 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ description: "Version to release (must match lib/asciichem/version.rb)"
8
+ required: true
9
+ type: string
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ environment: release
15
+ permissions:
16
+ contents: read
17
+ packages: write
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ ref: main
22
+ - name: Verify version matches input
23
+ run: |
24
+ actual=$(ruby -e 'require "./lib/asciichem/version"; print AsciiChem::VERSION')
25
+ if [ "$actual" != "${{ inputs.version }}" ]; then
26
+ echo "Version mismatch: input=${{ inputs.version }}, lib/asciichem/version.rb=$actual"
27
+ exit 1
28
+ fi
29
+ echo "Releasing $actual"
30
+ - uses: ruby/setup-ruby@v1
31
+ with:
32
+ ruby-version: "3.4"
33
+ bundler-cache: true
34
+ - name: Build gem
35
+ run: bundle exec rake build
36
+ - name: Set up RubyGems credentials
37
+ env:
38
+ RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
39
+ run: |
40
+ mkdir -p $HOME/.gem
41
+ cat > $HOME/.gem/credentials <<EOF
42
+ ---
43
+ :rubygems_api_key: ${RUBYGEMS_API_KEY}
44
+ EOF
45
+ chmod 0600 $HOME/.gem/credentials
46
+ - name: Push to RubyGems
47
+ run: |
48
+ gem push pkg/asciichem-${{ inputs.version }}.gem
49
+ - name: Summary
50
+ run: |
51
+ echo "Released asciichem ${{ inputs.version }} to RubyGems"
52
+ echo "https://rubygems.org/gems/asciichem"
data/ARCHITECTURE.adoc CHANGED
@@ -237,3 +237,156 @@ Plurimath handles it.
237
237
  * No hand-rolled model serialisation. The model is in-memory only;
238
238
  rendering to MathML / HTML / etc. is one-way output via Nokogiri or
239
239
  string building, not `(de)serialisation`.
240
+
241
+
242
+ == Native CML wire (v0.9-v0.11)
243
+
244
+ All AsciiChem model classes have native CML wire representations
245
+ (chemicalml 0.3.0+). The mapping is one-way in production: the
246
+ adapter produces native wire objects; the CML Translator enriches
247
+ with aci: extensions only for backwards-compat fallback.
248
+
249
+ [cols="1,1,2", options="header"]
250
+ |===
251
+ | Model class | Wire container | Native child elements
252
+ | Crystal | | with cells, , (fractional coords)
253
+ | Spectrum | | with of (xValue/yValue/yMultiplicity/title)
254
+ | ZMatrix | | (text content; structural length/angle/torsion children are future work)
255
+ | Calculation | | of with (value, units)
256
+ | Mechanism | | (text via title; structural step list is future work)
257
+ | Reaction conditions | | with (above/below)
258
+ |===
259
+
260
+ == Extension channels (six parallel modules)
261
+
262
+ Each channel is a MECE module under :
263
+
264
+ * — per-atom aci: attributes (FIELDS registry)
265
+ * — top-level constructs via aci: elements (HANDLERS registry)
266
+ * — inside molecules (BRACKETS registry on Group)
267
+ * — preserves unknown top-level elements verbatim
268
+ * — per-molecule key/value metadata (aci:meta-*)
269
+ * — now mostly legacy; native <conditionList> preferred
270
+
271
+ The is a thin orchestrator (~80 lines) that calls each
272
+ module's // in pipeline order.
273
+
274
+ == Model-owned wire codes
275
+
276
+ Wire-format mappings live on the model classes as the single source
277
+ of truth:
278
+
279
+ * / /
280
+ * /
281
+ * /
282
+ * / / /
283
+ * (shared by adapter and extension modules)
284
+
285
+ Adding a new bond kind, bracket kind, arrow kind, or cell parameter
286
+ is one entry on the model; both adapter directions pick it up.
287
+
288
+ == Linter framework
289
+
290
+ Ten registered checks (auto-load via ):
291
+
292
+ * — stoichiometric atom conservation
293
+ * — charge conservation (v0.7+)
294
+ * — group bracket consistency
295
+ * — cell lengths/angles/fractional coords (v0.6+)
296
+ * — forward references and geometry (v0.6+)
297
+ * — peak data validation (v0.6+)
298
+ * — periodic-table membership
299
+ * — isotope mass >= atomic number
300
+ * — unmatched ring-closure digits
301
+ * — bond order + charge <= max valence
302
+
303
+ sorts by severity then deduplicates.
304
+
305
+ == Convenience methods on Molecule
306
+
307
+ * — total atoms (recurses through groups/nested molecules)
308
+ * — Hill-system canonical formula (C first, H second, alpha)
309
+ * — sum of atomic_mass × count (v0.13+)
310
+
311
+ == Native CML wire (v0.9-v0.11)
312
+
313
+ All AsciiChem model classes have native CML wire representations
314
+ (chemicalml 0.3.0+). The adapter produces native wire objects;
315
+ the CML Translator enriches with aci: extensions only as a
316
+ backwards-compat fallback.
317
+
318
+ |===
319
+ | Model class | Wire container | Native child elements
320
+
321
+ | Crystal
322
+ | `<molecule>`
323
+ | `<crystal>` with `<scalar>` cells, `<symmetry>`, `<atomArray>` (fractional coords)
324
+
325
+ | Spectrum
326
+ | `<molecule>`
327
+ | `<spectrum>` with `<peakList>` of `<peak>` (xValue/yValue/yMultiplicity/title)
328
+
329
+ | ZMatrix
330
+ | `<molecule>`
331
+ | `<zMatrix>` (text content; structural length/Angle/Torsion children are future work)
332
+
333
+ | Calculation
334
+ | `<molecule>`
335
+ | `<propertyList>` of `<property>` with `<scalar>` (value, units)
336
+
337
+ | Mechanism
338
+ | `<reaction>`
339
+ | `<mechanism>` (text via title; structural step list is future work)
340
+
341
+ | Reaction conditions
342
+ | `<reaction>`
343
+ | `<conditionList>` with `<scalar>` (above/below)
344
+ |===
345
+
346
+ == Six extension channels (MECE modules under Cml)
347
+
348
+ * `Extensions::AtomAttributes` — per-atom aci: attributes (FIELDS registry)
349
+ * `Extensions::TopLevel` — top-level constructs via aci: elements (HANDLERS registry)
350
+ * `GroupExtensions` — `<aci:group>` inside molecules
351
+ * `OpaqueExtensions` — preserves unknown top-level elements verbatim
352
+ * `MetadataExtensions` — per-molecule key/value metadata (aci:meta-*)
353
+ * `ConditionsExtensions` — legacy; native <conditionList> preferred (v0.11+)
354
+
355
+ The `Translator` is a thin orchestrator (~80 lines) that calls each
356
+ module's inject/extract/restore in pipeline order.
357
+
358
+ == Model-owned wire codes (single source of truth)
359
+
360
+ Wire-format mappings live on the model classes:
361
+
362
+ * `Bond::CML_ORDER_CODES` / `KIND_BY_CML_ORDER` / `CML_STEREO_CODES`
363
+ * `Group::BRACKETS` / `BRACKET_BY_WIRE`
364
+ * `Reaction::ARROWS` / `ARROW_BY_WIRE`
365
+ * `Crystal::CELL_PARAMS` / `LENGTH_FIELDS` / `ANGLE_FIELDS` / `CELL_LABELS`
366
+ * `Cml::ID_PREFIXES` (shared by adapter and extension modules)
367
+
368
+ Adding a new bond kind, bracket kind, arrow kind, or cell parameter
369
+ is one entry on the model; both adapter directions pick it up.
370
+
371
+ == Linter framework
372
+
373
+ Ten registered checks (auto-load via `Linter::Base.register`):
374
+
375
+ * `BalanceCheck` — stoichiometric atom conservation
376
+ * `ChargeBalanceCheck` — charge conservation (v0.7+)
377
+ * `BracketBalanceCheck` — group bracket consistency
378
+ * `CrystalSanityCheck` — cell lengths/angles/fractional coords (v0.6+)
379
+ * `ZMatrixReferenceCheck` — forward references and geometry (v0.6+)
380
+ * `SpectrumPeakCheck` — peak data validation (v0.6+)
381
+ * `ElementValidationCheck` — periodic-table membership
382
+ * `IsotopeSanityCheck` — isotope mass >= atomic number
383
+ * `UnclosedRingCheck` — unmatched ring-closure digits
384
+ * `ValenceCheck` — bond order + charge <= max valence
385
+
386
+ `Linter.run` sorts by severity (errors first) then deduplicates.
387
+
388
+ == Convenience methods on Molecule
389
+
390
+ * `#atom_count` — total atoms (recurses through groups/nested molecules)
391
+ * `#hill_formula` — Hill-system canonical formula (C first, H second, alpha)
392
+ * `#formula_weight` — sum of atomic_mass * count (v0.13+)
data/CHANGELOG.md CHANGED
@@ -3,15 +3,373 @@
3
3
  All notable changes to AsciiChem are documented here.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
- ## [Unreleased]
6
+ ## [0.19.0] - 2026-09-09
7
+
8
+ ### Added
9
+ - `AsciiChem::Identifiers` — offline format validators for chemical
10
+ identifier conventions, self-registering like linter checks
11
+ (`cas` with check digit, `inchi` with formula-layer analysis,
12
+ `inchikey` 14-10-1 format, `smiles` structural sanity). Purely
13
+ lexical; no network, no assignment checking.
14
+ - `IdentifierFormatCheck` (linter) — validates `@cas`/`@inchi`/
15
+ `@inchikey`/`@smiles` annotation values against the registry;
16
+ unknown conventions are skipped.
17
+ - `IdentifierConsistencyCheck` (linter) — cross-checks identifier
18
+ values against the molecule they annotate: InChI formula-layer
19
+ element counts vs molecule composition (error), SMILES element set
20
+ vs molecule elements with implicit hydrogens tolerated (warning).
21
+ - `Model::Molecule#element_counts(with_coefficient:)` — public
22
+ composition API; `#hill_formula` and `#formula_weight` now share it.
23
+
24
+ ### Fixed
25
+ - RSpec suite aborted non-deterministically mid-run (randomly
26
+ truncated example counts, e.g. 56–748 of 788): CLI commands call
27
+ `Kernel#exit` even on success, and the cli specs' `run` helper did
28
+ not trap `SystemExit`, so under random ordering an uncaught exit
29
+ aborted the suite while reporting "0 failures". The helper now
30
+ traps it; five consecutive full runs execute all 788 examples.
31
+ - Linter registry re-registration in specs now globs all check files
32
+ instead of a hardcoded list (new checks no longer vanish from the
33
+ suite after `Registry.reset`).
34
+
35
+ ## [0.18.1] - 2026-07-21
36
+
37
+ ### Changed
38
+ - Updated gemspec summary and description to mention all
39
+ beyond-formulas constructs (crystals, spectra, calculations,
40
+ Z-matrices, reaction mechanisms) and the native CML round-trip
41
+ via chemicalml.
42
+ - Added `docs_uri` metadata pointing to the AsciiChem documentation
43
+ site.
44
+
45
+ ## [0.18.0] - 2026-07-21
46
+
47
+ ### Changed
48
+ - Beyond-formulas body validation: MechanismBuilder, SpectrumBuilder,
49
+ and CalculationBuilder now validate each body line at parse time.
50
+ Malformed entries (missing `:` separator) raise `ParseError` with
51
+ the offending line number and content, instead of being silently
52
+ dropped.
53
+
54
+ ## [0.17.0] - 2026-07-21
55
+
56
+ ### Added
57
+ - `Atom::Point3` value object (Struct with `x`, `y`, `z`, `to_a`,
58
+ `magnitude`).
59
+ - `Atom#cartesian` and `Atom#fractional` accessors return Point3
60
+ bundling the flat coordinate fields. Backwards compatible: flat
61
+ accessors (`x2`/`y2`/`z2`, `x_fract`/`y_fract`/`z_fract`) remain.
62
+
63
+ ## [0.16.0] - 2026-07-21
64
+
65
+ ### Added
66
+ - `Node#to_structural_svg` shortcut — invokes
67
+ `Formatter::StructuralSvg` for 2D diagram output. For Crystal
68
+ nodes, projects fractional coordinates onto the ab-plane with a
69
+ unit cell outline; for molecules with bonds, uses elkrb for
70
+ graph layout (existing behaviour); falls back to linear Svg
71
+ otherwise.
72
+ - `Formatter::StructuralSvg#visit_crystal` — renders a Crystal as
73
+ a 2D projection of fractional coordinates with unit cell outline.
74
+
75
+ ## [0.15.0] - 2026-07-21
76
+
77
+ ### Changed
78
+ - Parser error messages now report `line N, col M` for multi-line
79
+ inputs instead of `char N`. The caret pointer is positioned on
80
+ the relevant line. Single-line inputs continue to use the
81
+ existing char-position format.
82
+
83
+ ## [0.14.0] - 2026-07-21
84
+
85
+ ### Added
86
+ - `lint -f json` CLI option — emits diagnostics as a JSON array for
87
+ tooling integration (CI, editors, downstream scripts). Each
88
+ entry has `severity`, `message`, and `node` fields.
89
+
90
+ ## [0.13.0] - 2026-07-21
91
+
92
+ ### Added
93
+ - `Molecule#formula_weight` — sum of `atomic_mass × count` across
94
+ all atoms (recurses through groups and nested molecules). Returns
95
+ nil if any element lacks atomic mass data. Examples:
96
+ `parse("H_2O").nodes.first.formula_weight` returns `18.015`;
97
+ `parse("C_6H_12O_6")` returns `180.156`.
98
+ - `PeriodicTable.atomic_mass(symbol)` — IUPAC 2021 standard atomic
99
+ weights for ~50 common elements. Single source of truth for
100
+ atomic mass data.
101
+
102
+ ### Changed
103
+ - `PeriodicTable::Element` struct gains `atomic_mass` field (nil
104
+ when unpopulated).
105
+
106
+ ## [0.12.0] - 2026-07-21
107
+
108
+ ### Added
109
+ - `Molecule#hill_formula` — Hill-system canonical formula (C first,
110
+ then H, then others alphabetically). Useful for indexing and
111
+ display. `parse("C_2H_6O").nodes.first.hill_formula` returns
112
+ `"C2H6O"`; `parse("H_2SO_4")` returns `"H2O4S"`.
113
+
114
+ ### Changed
115
+ - `Linter.run` sorts diagnostics by severity (errors first) then by
116
+ message for stable UX output. Also deduplicates identical
117
+ (message, node) pairs from different checks.
118
+ - `AsciiChem::Parser` caches `Grammar` and `Transform` instances at
119
+ the class level instead of allocating per parse. Benchmark: ~15%
120
+ throughput improvement on repeated parses.
121
+
122
+ ## [0.11.0] - 2026-07-21
123
+
124
+ ### Added
125
+ - **Native CML wire for ZMatrix, Calculation, Mechanism, and
126
+ reaction conditions.** Completes the native-wire migration arc
127
+ for all beyond-formulas constructs:
128
+ - ZMatrix emits as `<molecule>` with `<zMatrix>` child carrying
129
+ the text form (structural length/angle/torsion children are a
130
+ future enhancement).
131
+ - Calculation emits as `<molecule>` with `<propertyList>` of
132
+ `<property>` children, each with a `<scalar>` carrying value
133
+ and units.
134
+ - Mechanism emits as `<reaction>` with `<mechanism>` child.
135
+ Currently uses the text form via title attribute; structural
136
+ step-by-step representation is a future enhancement.
137
+ - Reaction conditions (`<=>[Fe][400C]`) now emit as native
138
+ `<conditionList>` with `<scalar>` children for above/below,
139
+ replacing the `aci:conditionsAbove`/`aci:conditionsBelow`
140
+ attributes used in v0.10.0 and earlier.
141
+
142
+ ### Changed
143
+ - `ConditionsExtensions.inject`: now a no-op when native
144
+ `<conditionList>` is present (defensive fallback only).
145
+ - `ModelAdapter::FromCanonical`: reads native `<conditionList>`
146
+ via `conditions_from_canonical`.
147
+
148
+ ## [0.10.0] - 2026-07-21
149
+
150
+ ### Added
151
+ - **Native CML wire for Spectrum.** Spectrum nodes now emit as a
152
+ `<molecule>` containing a native `<spectrum>` child (with `format`
153
+ and `condition` attributes) plus a `<peakList>` of `<peak>`
154
+ elements. Each peak carries `xValue`, `yValue`, `yMultiplicity`,
155
+ and `title` (assignment) attributes per the CML Schema 3 spec.
156
+
157
+ ### Changed
158
+ - `ModelAdapter::ToCanonical`: new `spectrum_to_canonical` mapper
159
+ wraps Spectrum in a Molecule wire object with spectrum child.
160
+ - `ModelAdapter::FromCanonical`: detects molecule-with-spectrum and
161
+ rebuilds as Spectrum node.
162
+ - `Translator::NATIVELY_WIRED`: now includes Spectrum.
163
+
164
+ ## [0.9.0] - 2026-07-21
165
+
166
+ ### Added
167
+ - **Native CML wire for Crystal.** Crystal nodes now emit as a
168
+ `<molecule>` containing a native `<crystal>` child (with `<scalar>`
169
+ cells and `<symmetry spaceGroup=...>`) plus an `<atomArray>` with
170
+ fractional coordinates. Other CML tools can now read AsciiChem
171
+ crystal output directly without needing the aci: extension
172
+ namespace. Round-trip preserves all fields. The legacy aci:
173
+ text-carrier form is still accepted on parse for backwards
174
+ compatibility.
175
+
176
+ ### Changed
177
+ - `ModelAdapter::ToCanonical`: new `crystal_to_canonical` mapper
178
+ wraps Crystal in a Molecule wire object.
179
+ - `ModelAdapter::FromCanonical`: detects molecule-with-crystal and
180
+ rebuilds as Crystal node.
181
+ - `Extensions::TopLevel.collect`: accepts `skip_classes:` parameter
182
+ to suppress the aci: text carrier for constructs with native wire.
183
+ - `Translator::NATIVELY_WIRED`: registry of construct classes that
184
+ have been migrated to native wire (currently just Crystal).
185
+
186
+ ## [0.8.0] - 2026-07-21
187
+
188
+ ### Changed
189
+ - `chemicalml` dependency bumped from `~> 0.2.1` to `~> 0.3.0`.
190
+ chemicalml 0.3.0 fixes the wire serialization gaps that blocked
191
+ Phases 06-10, 15 (native CML wire for Crystal/Spectrum/ZMatrix/
192
+ Mechanism/Calculation/conditions). Molecule wire now serializes
193
+ `<crystal>`, `<spectrum>`, `<zMatrix>`, `<propertyList>` children;
194
+ Reaction wire now serializes `<conditionList>`, `<spectatorList>`,
195
+ `<mechanism>` children.
196
+
197
+ ### Added
198
+ - `.github/workflows/release.yml` — manual-dispatch release workflow
199
+ that pushes the gem to RubyGems. Triggered via `gh workflow run
200
+ release.yml -f version=X.Y.Z` after the version-bump PR merges.
201
+ Verifies the input version matches `lib/asciichem/version.rb`.
202
+
203
+ ## [0.7.0] - 2026-07-21
204
+
205
+ ### Added
206
+ - Implicit subscripts on Hydrogen: `H2` now parses as `H_2`, so
207
+ users can write `H2O`, `CH4`, `NH3` without explicit underscores.
208
+ Resolves the long-pending feature request without breaking
209
+ SMILES-style ring closures on other elements (Hydrogen cannot
210
+ form ring closures — only 1 bond — so bare digits after H are
211
+ unambiguously subscripts).
212
+
213
+ ### Changed
214
+ - Grammar: new `hydrogen_atom` rule placed before `plain_atom` in
215
+ the unit dispatch. The `match('[a-z]').absent?` lookahead ensures
216
+ `He`, `Ho`, etc. fall through to plain_atom unchanged.
217
+
218
+ ## [0.6.0] - 2026-07-20
219
+
220
+ ### Added
221
+ - `Model::OpaqueCml` node + `Cml::OpaqueExtensions` module: preserve
222
+ unknown CML elements through round-trip as opaque blobs.
223
+ - `Linter::ChargeBalanceCheck`: validates charge conservation in
224
+ reactions (parallel to BalanceCheck's atom conservation).
225
+ - `Linter::CrystalSanityCheck`, `ZMatrixReferenceCheck`,
226
+ `SpectrumPeakCheck`: validate cell parameters, ZMatrix references,
227
+ and spectrum peak data.
228
+ - `Molecule#atom_count`: total atom count recursing through groups
229
+ and nested molecules with subscripts and multiplicities applied.
230
+ - `Model::Molecule::Property`, `Meta`, `Label`, `Formula` Structs
231
+ replace Hash-with-magic-keys fields.
232
+ - `Model::Spectrum::Peak`, `Calculation::Property`, `Mechanism::Step`
233
+ Structs replace Hash-with-magic-keys fields.
234
+ - Formatter visit methods for `Crystal`, `Spectrum`, `Calculation`,
235
+ `ZMatrix`, `Mechanism`, `OpaqueCml` across MathML, HTML, LaTeX, SVG.
236
+ - CLI smoke coverage for beyond-formulas constructs.
237
+ - Fuzz corpus files 16-20 (crystal, spectrum, calculation, zmatrix,
238
+ mechanism).
239
+ - Benchmark cases for beyond-formulas constructs.
240
+
241
+ ### Changed
242
+ - `Cml::Extensions` split into `Extensions::AtomAttributes` and
243
+ `Extensions::TopLevel` sub-modules; facade preserves public API.
244
+ - `Cml::MetadataExtensions` and `Cml::ConditionsExtensions` extracted
245
+ from inline Translator code as parallel extension channels.
246
+ - `Cml::ID_PREFIXES` is the single source of truth for canonical ID
247
+ prefixes (atom/bond/molecule/reaction/group).
248
+ - `Extensions.ensure_namespace(root)` shared helper replaces six
249
+ duplicated `namespace_declared?` / `ensure_namespace` methods.
250
+ - `Model::Bond` owns `CML_ORDER_CODES`, `KIND_BY_CML_ORDER`,
251
+ `CML_STEREO_CODES` (was duplicated across adapter directions).
252
+ - `Model::Group` owns `BRACKETS` with `{open, close, wire}` attrs.
253
+ - `Model::Reaction` owns `ARROW_BY_WIRE` inverse map.
254
+ - `Model::Crystal` owns `CELL_PARAMS`, `LENGTH_FIELDS`, `ANGLE_FIELDS`,
255
+ `CELL_LABELS`, `#each_cell_param(format)` as single source for cell
256
+ parameter labels across formatters.
257
+ - `Extensions::TopLevel::Handler.text_round_trip` and
258
+ `.source_with_wrapper` factory methods eliminate duplicate lambdas
259
+ across 8 handlers.
260
+ - `Transform::BuilderHelpers` shared module included by all 5 builder
261
+ classes.
262
+ - `Linter::Base.register(:name)` pattern — adding a check is one new
263
+ file + one autoload entry.
264
+
265
+ ### Fixed
266
+ - Empty body braces (`crystal[x]{}`, `spectrum[nmr](){}`, etc.) now
267
+ parse correctly for all 5 beyond-formulas constructs. Root cause:
268
+ parslet represents zero-match `.repeat` as `[]` not `""`.
269
+ - Cascade step conditions (`A ->[c1] B ->[c2] C`) now survive CML
270
+ round-trip. Previously the inline translator code only walked
271
+ top-level reactions.
272
+
273
+ ## [0.5.1] - 2026-07-16
274
+
275
+ ### Added
276
+ - CML round-trip for all five beyond-formulas constructs via aci:
277
+ namespace extension elements.
278
+
279
+ ## [0.5.0] - 2026-07-16
280
+
281
+ ### Added
282
+ - Phase 5: reaction mechanism syntax (`mechanism{step1: A -> B;
283
+ spectator: Na+}`).
284
+
285
+ ## [0.4.1] - 2026-07-16
286
+
287
+ ### Added
288
+ - Phase 3: computational chemistry results (`calc(method/basis){...}`).
289
+ - Phase 4: Z-Matrix internal coordinates (`zmatrix{C1; H2 C1 1.09; ...}`).
290
+
291
+ ## [0.4.0] - 2026-07-16
292
+
293
+ ### Added
294
+ - Phase 1: crystallography syntax (`crystal[NaCl](a=...,sg=...){...}`).
295
+ - Phase 2: spectroscopy syntax (`spectrum[nmr](type=1H,...){...}`).
296
+
297
+ ## [0.3.4] - 2026-07-16
298
+
299
+ ### Added
300
+ - Spin multiplicity (`C@m(2)`), atom title (`C@t("alpha")`), and
301
+ fractional coordinates (`C@f(0.5,0.5,0.5)`) syntax.
302
+
303
+ ## [0.3.3] - 2026-07-16
304
+
305
+ ### Fixed
306
+ - Property and metadata CML round-trip for chemicalml 0.2.1.
307
+
308
+ ## [0.3.2] - 2026-07-16
309
+
310
+ ### Added
311
+ - Reaction conditions CML round-trip via aci: attributes.
312
+
313
+ ## [0.3.1] - 2026-07-16
314
+
315
+ ### Changed
316
+ - Adapted to chemicalml 0.2.1 (wire classes ARE the model).
317
+
318
+ ## [0.3.0] - 2026-07-15
319
+
320
+ ### Added
321
+ - Full CML support via chemicalml gem.
322
+ - aci: extension namespace for fields CML doesn't natively carry.
323
+ - New syntax: spin multiplicity, atom titles, fractional coordinates,
324
+ molecule annotations (`@name`, `@inchi`, `@meta`, `@formula`,
325
+ `@label`, `@mw`, `@title`).
326
+ - Group preservation through CML round-trip via `<aci:group>`.
327
+ - ElectronConfiguration and EmbeddedMath top-level constructs via aci:.
328
+ - `parse-cml` CLI subcommand.
329
+
330
+ ## [0.2.0] - 2026-07-14
331
+
332
+ ### Added
333
+ - Layout module (MoleculeWalker / GraphBuilder / ResultExtractor)
334
+ using elkrb for 2D structural SVG.
335
+ - StructuralSvg formatter with bond-kind renderer registry.
336
+ - Ring closures (SMILES-style): `C1-C-C-C-C-C1` is cyclohexane.
337
+
338
+ ## [0.1.0] - 2026-07-10
7
339
 
8
340
  ### Added
9
341
  - Initial gem scaffold: autoload tree, version, errors.
10
- - Core model: `Formula`, `Atom`, `Molecule`, `Group`, `Bond`, `Reaction`,
11
- `ElectronConfiguration`, `EmbeddedMath`, `Text`.
342
+ - Core model: `Formula`, `Atom`, `Molecule`, `Group`, `Bond`,
343
+ `Reaction`, `ElectronConfiguration`, `EmbeddedMath`, `Text`.
12
344
  - Parslet parser and transform for v1 constructs.
13
- - Formatters: MathML and Text (round-trip).
14
- - Thor-based CLI: `convert`, `roundtrip`, `version`.
345
+ - Formatters: MathML, Text (round-trip), HTML, LaTeX, SVG.
346
+ - Thor-based CLI: `convert`, `roundtrip`, `lint`, `parse-cml`,
347
+ `version`.
15
348
  - Comprehensive RSpec suite with round-trip conformance.
16
349
 
17
350
  [Unreleased]: https://github.com/asciichem/asciichem-ruby/commits/main
351
+ [0.18.1]: https://github.com/asciichem/asciichem-ruby/compare/v0.18.0...v0.18.1
352
+ [0.18.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.17.0...v0.18.0
353
+ [0.17.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.16.0...v0.17.0
354
+ [0.16.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.15.0...v0.16.0
355
+ [0.15.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.14.0...v0.15.0
356
+ [0.14.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.13.0...v0.14.0
357
+ [0.13.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.12.0...v0.13.0
358
+ [0.12.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.11.0...v0.12.0
359
+ [0.11.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.10.0...v0.11.0
360
+ [0.10.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.9.0...v0.10.0
361
+ [0.9.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.8.0...v0.9.0
362
+ [0.8.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.7.0...v0.8.0
363
+ [0.7.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.6.0...v0.7.0
364
+ [0.6.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.5.1...v0.6.0
365
+ [0.5.1]: https://github.com/asciichem/asciichem-ruby/compare/v0.5.0...v0.5.1
366
+ [0.5.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.4.1...v0.5.0
367
+ [0.4.1]: https://github.com/asciichem/asciichem-ruby/compare/v0.4.0...v0.4.1
368
+ [0.4.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.4...v0.4.0
369
+ [0.3.4]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.3...v0.3.4
370
+ [0.3.3]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.2...v0.3.3
371
+ [0.3.2]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.1...v0.3.2
372
+ [0.3.1]: https://github.com/asciichem/asciichem-ruby/compare/v0.3.0...v0.3.1
373
+ [0.3.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.2.0...v0.3.0
374
+ [0.2.0]: https://github.com/asciichem/asciichem-ruby/compare/v0.1.0...v0.2.0
375
+ [0.1.0]: https://github.com/asciichem/asciichem-ruby/releases/tag/v0.1.0
data/asciichem.gemspec CHANGED
@@ -8,10 +8,12 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Ribose Inc."]
9
9
  spec.email = ["open.source@ribose.com"]
10
10
 
11
- spec.summary = "ASCII syntax for chemistry formulae, reactions, and structures."
11
+ spec.summary = "ASCII syntax for chemistry formulae, reactions, structures, and spectroscopy."
12
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."
13
+ "reactions, electron configurations, bonds, crystals, spectra, " \
14
+ "calculations, Z-matrices, and reaction mechanisms. It parses to a " \
15
+ "semantic model and renders to MathML, HTML, LaTeX, SVG, and CML. " \
16
+ "Math embedding uses Plurimath. Round-trips to native CML via chemicalml."
15
17
  spec.homepage = "https://www.asciichem.org"
16
18
  spec.license = "BSD-2-Clause"
17
19
  spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
@@ -19,6 +21,7 @@ Gem::Specification.new do |spec|
19
21
  spec.metadata["homepage_uri"] = spec.homepage
20
22
  spec.metadata["source_code_uri"] = "https://github.com/asciichem/asciichem-ruby"
21
23
  spec.metadata["changelog_uri"] = "https://github.com/asciichem/asciichem-ruby/blob/main/CHANGELOG.md"
24
+ spec.metadata["docs_uri"] = "https://www.asciichem.org"
22
25
  spec.metadata["rubygems_mfa_required"] = "true"
23
26
 
24
27
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
@@ -30,7 +33,7 @@ Gem::Specification.new do |spec|
30
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
34
  spec.require_paths = ["lib"]
32
35
 
33
- spec.add_dependency "chemicalml", "~> 0.2.1"
36
+ spec.add_dependency "chemicalml", "~> 0.3.0"
34
37
  spec.add_dependency "elkrb", "~> 1.0"
35
38
  spec.add_dependency "nokogiri", "~> 1.16"
36
39
  spec.add_dependency "parslet", "~> 2.0"
@@ -31,7 +31,12 @@ CASES = {
31
31
  "equilibrium (Haber)" => "N_2 + 3H_2 <=>[Fe][400°C] 2NH_3",
32
32
  "cascade (A -> B -> C -> D)" => "A -> B -> C -> D",
33
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"
34
+ "bonds (H-O-H=O#H)" => "H-O-H=O#H",
35
+ "crystal (NaCl)" => "crystal[NaCl](a=5.64,b=5.64,c=5.64,alpha=90,beta=90,gamma=90,sg=Fm-3m){Na@f(0,0,0) Cl@f(0.5,0.5,0.5)}",
36
+ "spectrum (NMR)" => %(spectrum[nmr](type=1H,solvent=CDCl3){1.2: 3H s "CH3"}),
37
+ "calculation (DFT)" => "calc(b3lyp/6-31G*){energy: -234.5 Hartree}",
38
+ "zmatrix (methane)" => "zmatrix{\n C1\n H2 C1 1.09\n H3 C1 1.09 H2 109.5\n H4 C1 1.09 H2 109.5 H3 120.0\n}",
39
+ "mechanism (2-step)" => "mechanism{\n step1: A + B -> C\n step2: C -> D + E\n spectator: Na+\n}"
35
40
  }.freeze
36
41
 
37
42
  def parse_bench(label, source)