Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ source "https://rubygems.org"
gemspec

gem "canon"
gem "lutaml-model", "~> 0.8.0", github: "lutaml/lutaml-model", branch: "main"
gem "lutaml-model",
github: "lutaml/lutaml-model",
branch: "fix/global-context-register-lookup-fallback"
gem "mml"
gem "oga"
gem "ox"
gem "plurimath", github: "plurimath/plurimath", branch: "rt-lutaml-080"
gem "plurimath", github: "plurimath/plurimath",
branch: "feat/autoload-and-mml-update"
gem "pry"
gem "rake"
gem "rspec"
Expand All @@ -18,3 +22,7 @@ gem "rubocop-performance"
gem "rubocop-rake"
gem "rubocop-rspec"
gem "simplecov"
gem "unitsdb",
github: "unitsml/unitsdb-ruby",
branch: "feat/context-register-models",
submodules: true
18 changes: 16 additions & 2 deletions docs/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,22 @@ The UnitsML Ruby library consists of several key components:
* Prefix: Represents unit prefixes (k, m, μ, etc.)
* Dimension: Represents physical dimensions

The library also includes the full https://github.com/unitsml/unitsdb[UnitsDB]
units database, which contains standard units, prefixes, and dimensions.
The library uses the https://github.com/unitsml/unitsdb-ruby[unitsdb-ruby]
gem as the primary runtime source for standard units, prefixes, and dimensions.

== Development setup

The development `Gemfile` pins the current integration branches for
`lutaml-model`, `mml`, `plurimath`, and `unitsdb-ruby`. If you need to test
coordinated local changes across those repositories, temporarily switch those
dependencies to Bundler `path` entries that point at sibling checkouts, for
example `../lutaml-model` or `../unitsdb-ruby`. Keep those local path changes
out of commits unless the branch intentionally requires them.

The standard Ruby runtime path goes through `::Unitsdb.database`. When
`unitsdb-ruby` cannot load its bundled `data/` directory, UnitsML falls back to
the packaged YAML files in the `unitsdb-ruby` gem's `vendor/unitsdb`
directory, rather than a `vendor/unitsdb` directory in this repository.


== Usage
Expand Down
47 changes: 3 additions & 44 deletions lib/unitsml.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# frozen_string_literal: true

require "lutaml/model"
require "unitsdb"
require "mml"

module Unitsml
module_function

autoload :Dimension, "unitsml/dimension"
autoload :Configuration, "unitsml/configuration"
autoload :Errors, "unitsml/errors"
autoload :Extender, "unitsml/extender"
autoload :Fenced, "unitsml/fenced"
autoload :FencedNumeric, "unitsml/fenced_numeric"
autoload :Formula, "unitsml/formula"
autoload :IntermediateExpRules, "unitsml/intermediate_exp_rules"
autoload :MathmlHelper, "unitsml/mathml_helper"
autoload :Model, "unitsml/model"
autoload :Namespace, "unitsml/namespace"
autoload :Number, "unitsml/number"
Expand All @@ -26,54 +28,11 @@ module Unitsml
autoload :Utility, "unitsml/utility"
autoload :VERSION, "unitsml/version"

REGISTER_ID = :unitsml_ruby

def parse(string)
Unitsml::Parser.new(string).parse
end

def register
@register ||= Lutaml::Model::GlobalRegister.lookup(REGISTER_ID)
end

def register_model(klass, id:)
register.register_model(klass, id: id)
end

def get_class_from_register(class_name)
register.get_class(class_name)
end

def register_type_substitution(from:, to:)
register.register_global_type_substitution(
from_type: from,
to_type: to,
)
end
end

Lutaml::Model::GlobalRegister.register(
Lutaml::Model::Register.new(Unitsml::REGISTER_ID),
)

{
Unitsdb::Unit => Unitsml::Unitsdb::Unit,
Unitsdb::Units => Unitsml::Unitsdb::Units,
Unitsdb::Prefixes => Unitsml::Unitsdb::Prefixes,
Unitsdb::Dimension => Unitsml::Unitsdb::Dimension,
Unitsdb::PrefixReference => Unitsml::Unitsdb::PrefixReference,
Unitsdb::DimensionDetails => Unitsml::Unitsdb::DimensionQuantity,
}.each do |key, value|
Unitsml.register_type_substitution(from: key, to: value)
end

[
[Unitsml::Unitsdb::Dimensions, :unitsdb_dimensions],
[Unitsml::Unitsdb::Prefixes, :unitsdb_prefixes],
[Unitsml::Unitsdb::Quantities, :unitsdb_quantities],
[Unitsml::Unitsdb::Units, :unitsdb_units],
].each { |klass, id| Unitsml.register_model(klass, id: id) }

Lutaml::Model::Config.configure do |config|
config.xml_adapter_type = RUBY_ENGINE == "opal" ? :oga : :ox
end
47 changes: 47 additions & 0 deletions lib/unitsml/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require "unitsdb"

module Unitsml
module Configuration
CONTEXT_ID = :unitsml_ruby

module_function

def context_id
CONTEXT_ID
end

def context(force_populate: false)
existing = ::Unitsdb::Config.find_context(context_id)
return existing if existing && !force_populate

build_context
end

def register_model(klass, id:)
registered_models[id.to_sym] = klass
end

def registered_models
@registered_models ||= {}
end

def build_context
::Unitsdb::Config.context # ensure unitsdb context exists

substitutions = registered_models.each_value.filter_map do |klass|
parent = klass.superclass
next if parent == Object

{ from_type: parent, to_type: klass }
end

::Unitsdb::Config.populate_context(
id: context_id,
fallback_to: [::Unitsdb::Config.context_id],
substitutions: substitutions,
)
end
end
end
14 changes: 10 additions & 4 deletions lib/unitsml/dimension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

module Unitsml
class Dimension
include MathmlHelper

attr_accessor :dimension_name, :power_numerator

def initialize(dimension_name, power_numerator = nil)
Expand All @@ -26,7 +28,7 @@ def dim_symbols
def to_mathml(options)
# MathML key's value in unitsdb/dimensions.yaml
# file includes mi tags only.
value = ::Mml::V4::Mi.from_xml(dim_symbols.mathml)
value = mml_v4_from_xml(:mi, dim_symbols.mathml)
method_name = if power_numerator
value = msup_tag(value, options)
:msup
Expand Down Expand Up @@ -63,7 +65,10 @@ def to_xml(_)
symbol: dim_instance.processed_symbol,
power_numerator: power_numerator&.raw_value || 1,
}
Model::DimensionQuantities.const_get(modelize(element_name)).new(attributes)
Model::DimensionQuantities.const_get(modelize(element_name)).new(
**attributes,
lutaml_register: Configuration.context.id,
)
end

def xml_instances_hash(options)
Expand Down Expand Up @@ -94,8 +99,9 @@ def element_name

def msup_tag(value, options)
mathml = power_numerator.to_mathml(options)
msup = ::Mml::V4::Msup.new(
mrow_value: [::Mml::V4::Mrow.new(mi_value: [value])],
msup = mml_v4_new(
:msup,
mrow_value: [mml_v4_new(:mrow, mi_value: [value])],
)
[mathml].flatten.each do |record|
record_values = msup.public_send("#{record[:method_name]}_value") || []
Expand Down
2 changes: 2 additions & 0 deletions lib/unitsml/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module Unitsml
module Errors
autoload :BaseError, "unitsml/errors/base_error"
autoload :OpalPayloadNotBundledError,
"unitsml/errors/opal_payload_not_bundled_error"
autoload :PlurimathLoadError, "unitsml/errors/plurimath_load_error"
end
end
11 changes: 11 additions & 0 deletions lib/unitsml/errors/opal_payload_not_bundled_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Unitsml
module Errors
class OpalPayloadNotBundledError < Unitsml::Errors::BaseError
def to_s
"[unitsml] Error: Opal database payload is not bundled."
end
end
end
end
4 changes: 3 additions & 1 deletion lib/unitsml/extender.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

module Unitsml
class Extender
include MathmlHelper

attr_accessor :symbol

def initialize(symbol)
Expand All @@ -18,7 +20,7 @@ def to_mathml(options)
extender = multiplier(options[:multiplier] || "⋅", unicode: true)
{
method_name: :mo,
value: ::Mml::V4::Mo.new(value: extender, rspace: rspace),
value: mml_v4_new(:mo, value: extender, rspace: rspace),
}
end

Expand Down
8 changes: 6 additions & 2 deletions lib/unitsml/fenced.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Unitsml
class Fenced
include FencedNumeric
include MathmlHelper

attr_reader :open_paren, :value, :close_paren

Expand Down Expand Up @@ -31,11 +32,14 @@ def to_mathml(options = {})
mathml = value.to_mathml(options)
return mathml unless options[:explicit_parenthesis]

fenced = ::Mml::V4::Mrow.new(mo_value: [::Mml::V4::Mo.new(value: open_paren)])
fenced = mml_v4_new(
:mrow,
mo_value: [mml_v4_new(:mo, value: open_paren)],
)
fenced.ordered = true
fenced.element_order ||= [xml_order_element("mo")]
[mathml].flatten.each { |record| add_math_element(fenced, record) }
fenced.mo_value << ::Mml::V4::Mo.new(value: close_paren)
fenced.mo_value << mml_v4_new(:mo, value: close_paren)
fenced.element_order << xml_order_element("mo")
{ method_name: :mrow, value: fenced }
end
Expand Down
32 changes: 14 additions & 18 deletions lib/unitsml/formula.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

module Unitsml
class Formula
include MathmlHelper

attr_accessor :value, :explicit_value, :root

def initialize(value = [],
Expand All @@ -29,15 +31,14 @@ def ==(other)
def to_mathml(options = {})
if root
options = update_options(options)
nullify_mml_models if plurimath_available?
math = ::Mml::V4::Math.new(display: "block")
math = mml_v4_new(:math, display: "block")
math.ordered = true
math.element_order ||= []
value.each do |instance|
process_value(math, instance.to_mathml(options))
end
generated_math = math.to_xml.gsub(%r{&amp;(.*?)(?=</)}, '&\1')
reset_mml_models if plurimath_available?
generated_math = math.to_xml(register: mml_v4_context.id)
.gsub(%r{&amp;(.*?)(?=</)}, '&\1')

Comment on lines 31 to 42
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to_mathml no longer calls nullify_mml_models / reset_mml_models, but the private methods remain in this file and are now unused. Consider removing them to reduce maintenance surface, or reintroduce the call site if they are still required for Plurimath/MML interoperability.

Copilot uses AI. Check for mistakes.
generated_math.force_encoding("UTF-8")
else
Expand Down Expand Up @@ -80,7 +81,7 @@ def to_plurimath(options = {})
:asciimath)
end

Plurimath::Math.parse(to_mathml(options), :mathml)
Plurimath::Math.parse(compact_mathml_for_plurimath(to_mathml(options)), :mathml)
end

def dimensions_extraction
Expand Down Expand Up @@ -151,7 +152,10 @@ def dimensions(dims, options)
dim_id = dims.map(&:generate_id).join
attributes = { id: "D_#{dim_id}" }
dims.each { |dim| attributes.merge!(dim.xml_instances_hash(options)) }
Model::Dimension.new(attributes).to_xml.force_encoding("UTF-8")
Model::Dimension.new(
**attributes,
lutaml_register: Configuration.context.id,
).to_xml.force_encoding("UTF-8")
end

def sort_dims(values)
Expand Down Expand Up @@ -195,18 +199,6 @@ def plurimath_available?
Plurimath.const_defined?(:Mathml)
end

def nullify_mml_models
return unless defined?(Plurimath::Mathml::Parser::CONFIGURATION)

Plurimath::Mathml::Parser::CONFIGURATION.each_key { |klass| klass.model(klass) }
end

def reset_mml_models
return unless defined?(Plurimath::Mathml::Parser::CONFIGURATION)

::Mml::V4::Configuration.custom_models = Plurimath::Mathml::Parser::CONFIGURATION
end

def process_value(math, mathml_instances)
case mathml_instances
when Array
Expand All @@ -224,5 +216,9 @@ def update_options(options)
options.merge(multiplier: multiplier,
explicit_parenthesis: explicit_parenthesis).compact
end

def compact_mathml_for_plurimath(mathml)
mathml.gsub(/>\s+</, "><").strip
end
end
end
Loading
Loading