From 33e8ea600da0f8d1bd6e3f512d68cce898a2d34b Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Wed, 15 Jul 2026 11:49:43 +0800 Subject: [PATCH] feat: register WordprocessingML substitutions for omml types OmmlIntegration module registers Uniword's WordprocessingML classes as substitutions for omml gem's minimal stubs via Register#register_global_type_substitution. Current substitutions: - Omml::Models::CTBr => Uniword::Wordprocessingml::Break This establishes the integration point architecture: omml owns the math schema, Uniword owns the WordprocessingML schema, and the two meet through register substitution rather than duplicate model definitions. Additional substitutions (CTRPr, CTFonts, etc.) can be added to the SUBSTITUTIONS hash as API compatibility is verified. --- lib/uniword.rb | 10 ++++++ lib/uniword/omml_integration.rb | 62 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 lib/uniword/omml_integration.rb diff --git a/lib/uniword.rb b/lib/uniword.rb index 16bd59d4..cd8f6f4f 100644 --- a/lib/uniword.rb +++ b/lib/uniword.rb @@ -46,10 +46,16 @@ module Uniword # directly, falling back to :default for Uniword's own types. Setting # Config.default_register means from_xml/to_xml calls without an explicit # register: argument use :uniword automatically. + # + # OmmlIntegration (autoloaded below) then substitutes omml's minimal + # WordprocessingML stubs with Uniword's richer classes via the Register's + # type substitution mechanism — single source of truth for WordprocessingML. require "omml" Omml::Configuration.register_in(:uniword) Lutaml::Model::Config.default_register = :uniword + autoload :OmmlIntegration, "uniword/omml_integration" + # Version constant autoload :VERSION, "uniword/version" autoload :ModelAttributeAccess, "uniword/model_attribute_access" @@ -344,4 +350,8 @@ def html_to_docx(html, path) from_html(html).to_file(path) end end + + # Register WordprocessingML substitutions after autoloads are declared + # so OmmlIntegration can resolve Uniword class constants via const_get. + OmmlIntegration.register end diff --git a/lib/uniword/omml_integration.rb b/lib/uniword/omml_integration.rb new file mode 100644 index 00000000..f1e6dcbb --- /dev/null +++ b/lib/uniword/omml_integration.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "omml" + +module Uniword + # Registers Uniword's WordprocessingML classes as substitutions for + # Omml::Models' minimal WordprocessingML stubs. When an Omml model + # (e.g. CTOMath) resolves a WordprocessingML type symbol (e.g. + # +:ct_br+), the Register's substitution table redirects resolution + # to the corresponding Uniword class. + # + # This keeps a single source of truth for WordprocessingML types: + # Uniword owns the rich, builder-friendly classes; omml owns the math + # schema. The two meet through register substitution, not through + # duplicate model definitions. + module OmmlIntegration + SUBSTITUTIONS = { + "Omml::Models::CTBr" => "Uniword::Wordprocessingml::Break", + }.freeze + + class << self + def register + ensure_omml_context + register_substitutions + end + + private + + def ensure_omml_context + return if Lutaml::Model::GlobalContext.context(:uniword) + + Omml::Configuration.register_in(:uniword) + Lutaml::Model::Config.default_register = :uniword + end + + def register_substitutions + register_handle = Lutaml::Model::GlobalRegister.lookup(:uniword) + return unless register_handle + + SUBSTITUTIONS.each do |omml_class_name, uniword_class_name| + omml_class = constantize(omml_class_name) + uniword_class = constantize(uniword_class_name) + next unless omml_class && uniword_class + + register_handle.register_global_type_substitution( + from_type: omml_class, + to_type: uniword_class, + ) + end + end + + def constantize(class_name) + parts = class_name.split("::") + parts.reduce(Object) do |scope, part| + scope.const_get(part) + end + rescue NameError + nil + end + end + end +end