class TiptapService # NOTE: node must be deep symbolized keys def self.used_tags_and_libelle_for(node, tags = Set.new) case node in type: 'mention', attrs: { id:, label: }, **rest tags << [id, label] in { content:, **rest } if content.is_a?(Array) content.each { used_tags_and_libelle_for(_1, tags) } in type:, **rest # noop end tags end def to_html(node, substitutions = {}) return '' if node.nil? children(node[:content], substitutions, 0) end def to_path(node, substitutions = {}) return '' if node.nil? children_path(node[:content], substitutions) end private def initialize @body_started = false end def children_path(content, substitutions) content.map { node_to_path(_1, substitutions) }.join end def node_to_path(node, substitutions) case node in type: 'paragraph', content: children_path(content, substitutions) in type: 'text', text:, **rest text.strip in type: 'mention', attrs: { id: }, **rest substitutions.fetch(id) { "--#{id}--" } end end def children(content, substitutions, level) content.map { node_to_html(_1, substitutions, level) }.join end def node_to_html(node, substitutions, level) if level == 0 && !@body_started && node[:type].in?(['paragraph', 'heading']) && node.key?(:content) @body_started = true body_start_mark = " class=\"body-start\"" end case node in type: 'header', content: "
#{children(content, substitutions, level + 1)}
" in type: 'footer', content:, **rest "#{children(content, substitutions, level + 1)}" in type: 'headerColumn', content:, **rest "#{children(content, substitutions, level + 1)}" in type: 'paragraph', content:, **rest "#{children(content, substitutions, level + 1)}

" in type: 'title', content:, **rest "#{children(content, substitutions, level + 1)}" in type: 'heading', attrs: { level: hlevel, **attrs }, content: "#{children(content, substitutions, level + 1)}" in type: 'bulletList', content: "
    #{children(content, substitutions, level + 1)}
" in type: 'orderedList', content: "
    #{children(content, substitutions, level + 1)}
" in type: 'listItem', content: "
  • #{children(content, substitutions, level + 1)}
  • " in type: 'text', text:, **rest if rest[:marks].present? apply_marks(text, rest[:marks]) else text end in type: 'mention', attrs: { id: }, **rest text = substitutions.fetch(id) { "--#{id}--" } if rest[:marks].present? apply_marks(text, rest[:marks]) else text end in { type: type } if ["paragraph", "title", "heading"].include?(type) && !node.key?(:content) # noop end end def text_align(attrs) if attrs.present? && attrs[:textAlign].present? " style=\"text-align: #{attrs[:textAlign]}\"" else "" end end def apply_marks(text, marks) marks.reduce(text) do |text, mark| case mark in type: 'bold' "#{text}" in type: 'italic' "#{text}" in type: 'underline' "#{text}" in type: 'strike' "#{text}" in type: 'highlight' "#{text}" end end end end