Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bundles of elements #18

Open
andrew2net opened this issue Jun 8, 2024 · 5 comments
Open

Bundles of elements #18

andrew2net opened this issue Jun 8, 2024 · 5 comments
Assignees
Labels
enhancement New feature or request

Comments

@andrew2net
Copy link

In grammar, which describes the Relaton data model, we have groups of elements included in other elements. For example:

  ...
  <define name="sup">
    <element name="sup">
      <zeroOrMore>
        <ref name="PureTextElement"/>
      </zeroOrMore>
    </element>
  </define>
  ...
  <define name="PureTextElement">
    <choice>
      <text/>
      <ref name="em"/>
      <ref name="strong"/>
      <ref name="sub"/>
      <ref name="sup"/>
      <ref name="tt"/>
      <ref name="underline"/>
      <ref name="strike"/>
      <ref name="smallcap"/>
      <ref name="br"/>
    </choice>
  </define>
  ...

With the ShaleI want to be able to use a pattern like:

class PureTextElement < Shale::Mapper
  attribute :text, Shale::Type::String
  attribute :em, Em
  attribute :strong, Strong
  ...
  xml do
    map_content to: :text
    map_element 'em', to: :em
    map_element 'strong', to: :strong
    ...
  end
end

class Sup < Shale::Mapper
  attribute :content, PureTextElement, collection: true

  xml do
    root 'sup'
    map_content to: :content
  end
end

We need to have any number of bundled elements (PureTextElements in the example) as children in any sequence. We need Shale to parse that sequence and render it in the same order.

@andrew2net andrew2net added the enhancement New feature or request label Jun 8, 2024
@HassanAkbar
Copy link
Member

@andrew2net I've tried the following and this is currently working in lutaml/shale

I've created the following classes

class PureTextElement < Shale::Mapper
  attribute :text, Shale::Type::String
  attribute :em, Shale::Type::String # Em
  attribute :strong, Shale::Type::String # Strong
  attribute :underline, Shale::Type::String # Underline

  xml do
    preserve_element_order true

    map_content to: :text

    map_element 'em', to: :em
    map_element 'strong', to: :strong
    map_element 'underline', to: :underline
  end
end

class Sup < Shale::Mapper
  attribute :pure_text_element, PureTextElement, collection: true

  xml do
    root 'sup'

    map_element 'PureTextElement', to: :pure_text_element
  end
end

require 'shale/adapter/nokogiri'
Shale.xml_adapter = Shale::Adapter::Nokogiri

then I tried to convert the following XML from and to xml through shale

xml = <<~XML
  <sup>
    <PureTextElement>
      Some text <strong>Bold text</strong><em>important text</em>
    </PureTextElement>
    <PureTextElement>
      Some text <em>Bold text</em><strong>important text</strong>
    </PureTextElement>
    <PureTextElement>
      Some text <strong>Bold text</strong><em>important text</em>
    </PureTextElement>
  </sup>
XML

obj = Sup.from_xml(x)
obj.to_xml(pretty: true)

and it generates the same output as input.

preserve_element_order true is our own extension attribute to shale to preserve the order of the elements so it outputs them in the same order as they are received.

Can you explain what else is missing from shale?

@andrew2net
Copy link
Author

@HassanAkbar it is almost good but the PureTextElement is the definition name, not an element name. XML should look like:

  <sup>
    Some text <strong>Bold text</strong><em>important text</em>
    Some text <em>Bold text</em><strong>important text</strong>
    Some text <strong>Bold text</strong><em>important text</em>
  </sup>

I've tried to map the PureTextElement as content but Shale only recognizes text, it ignores other elements.

@andrew2net
Copy link
Author

@HassanAkbar I found workaround but it looks not so elegant:

module Relaton
  module Model
    class PureTextElement
      def initialize(element)
        @element = element
      end

      def self.cast(value)
        value
      end

      def self.of_xml(node) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/MethodLength
        case node.name
        when "text"
          text = node.text.strip
          text.empty? ? nil : new(text)
        when "em" then new Em.of_xml(node)
        when "strong" then new Strong.of_xml(node)
        when "sub" then new Sub.of_xml(node)
        when "sup" then new Sup.of_xml(node)
        when "tt" then new Tt.of_xml(node)
        when "underline" then new Underline.of_xml(node)
        when "strike" then new Strike.of_xml(node)
        when "smallcap" then new Smallcap.of_xml(node)
        when "br" then new Br.of_xml(node)
        end
      end

      def add_to_xml(parent, doc)
        if @element.is_a? String
          doc.add_text(parent, @element)
        else
          parent << @element.to_xml
        end
      end

      module Mapper
        def self.included(base)
          base.class_eval do
            attribute :content, PureTextElement, collection: true

            xml do
              map_content to: :content, using: { from: :content_from_xml, to: :content_to_xml }
            end
          end
        end

        def content_from_xml(model, node)
          (node.instance_variable_get(:@node) || node).children.each do |n|
            next if n.text? && n.text.strip.empty?

            model.content << PureTextElement.of_xml(n)
          end
        end

        def content_to_xml(model, parent, doc)
          model.content.each do |e|
            e.add_to_xml parent, doc
          end
        end
      end
    end
  end
end

module Relaton
  module Model
    class Sup < Shale::Mapper
      include PureTextElement::Mapper

      @xml_mapping.instance_eval do
        root "sup"
      end
    end
  end
end

Is it possible to implement this functionality within Shale?
Also in some cases we need to add elements to a bundle:

  <define name="strike">
    <element name="strike">
      <zeroOrMore>
        <choice>
          <ref name="PureTextElement"/>
          <ref name="index"/>
          <ref name="index-xref"/>
        </choice>
      </zeroOrMore>
    </element>
  </define>

To implement this I use the code:

module Relaton
  module Model
    class Strike < Shale::Mapper
      class Content
        def initialize(elements = [])
          @elements = elements
        end

        def self.cast(value)
          value
        end

        def self.of_xml(node)
          elms = node.children.map do |n|
            case n.name
            when "index" then Index.of_xml n
            when "index-xref" then IndexXref.of_xml n
            else PureTextElement.of_xml n
            end
          end
          new elms
        end

        def add_to_xml(parent, doc)
          @elements.each { |e| e.add_to_xml parent, doc }
        end
      end

      attribute :content, Content, collection: true

      xml do
        root "strike"
        map_content to: :content, using: { from: :content_from_xml, to: :content_to_xml }
      end

      def content_from_xml(model, node)
        model.content << Content.of_xml(node.instance_variable_get(:@node) || node)
      end

      def content_to_xml(model, parent, doc)
        model.content.each { |e| e.add_to_xml parent, doc }
      end
    end
  end
end

@HassanAkbar
Copy link
Member

@andrew2net I've been looking into the code and @ronaldtse Suggestion in this ticket -> metanorma/sts-ruby#12.

Currently, I'm working on adding map_all_content and map_content_element methods to xml so the classes will look something like below.

class TextWithTags < Shale::Mapper
  attribute :content, Shale::Type::String
  attribute :strong, Shale::Type::String, collection: true
  attribute :em, Shale::Type::String, collection: true

  xml do
    root "text-with-tags"

    map_all_content to: :content
    map_content_element 'strong', to: :strong
    map_content_element 'em', to: :em
  end
end

@andrew2net
Copy link
Author

@HassanAkbar in the example the content, strong, and em elements are in predefined order. This issue is about having any number of the elements in any order. Will it be possible to achieve it with the update?

@opoudjis opoudjis removed their assignment Jun 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants