# frozen_string_literal: true 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).gsub('

', '') end def to_texts_and_tags(node, substitutions = {}) return '' if node.nil? children_texts_and_tags(node[:content], substitutions) end private def initialize @body_started = false end def children_texts_and_tags(content, substitutions) content.map { node_to_texts_and_tags(_1, substitutions) }.join end def node_to_texts_and_tags(node, substitutions) case node in type: 'paragraph', content: children_texts_and_tags(content, substitutions) in type: 'paragraph' # empty paragraph '' in type: 'text', text: text.strip in type: 'mention', attrs: { id:, label: } if substitutions.present? substitutions.fetch(id) { "--#{id}--" } else "#{label}" end 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:, **rest "#{children(content, substitutions, level + 1)}" in type: 'listItem', content: "
  • #{children(content, substitutions, level + 1)}
  • " in type: 'descriptionList', content: "
    #{children(content, substitutions, level + 1)}
    " in type: 'descriptionTerm', content:, **rest "#{children(content, substitutions, level + 1)}" in type: 'descriptionDetails', 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_or_presentation = substitutions.fetch(id) { "--#{id}--" } text = if text_or_presentation.respond_to?(:to_tiptap_node) handle_presentation_node(text_or_presentation, substitutions, level + 1) else text_or_presentation end 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 handle_presentation_node(presentation, substitutions, level) node = presentation.to_tiptap_node content = node_to_html(node, substitutions, level) if presentation.block_level? "

    #{content}

    " else content end end def text_align(attrs) if attrs.present? && attrs[:textAlign].present? " style=\"text-align: #{attrs[:textAlign]}\"" else "" end end def class_list(attrs) if attrs.present? && attrs[:class].present? " class=\"#{attrs[:class]}\"" 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