diff --git a/app/services/tiptap_service.rb b/app/services/tiptap_service.rb new file mode 100644 index 000000000..9514dc11c --- /dev/null +++ b/app/services/tiptap_service.rb @@ -0,0 +1,65 @@ +class TiptapService + class << self + def to_html(node, tags) + children(node[:content], tags) + end + + private + + def children(content, tags) + content.map { node_to_html(_1, tags) }.join + end + + def node_to_html(node, tags) + case node + in type: 'paragraph', content:, **rest + "#{children(content, tags)}

" + in type: 'heading', attrs: { level:, **attrs }, content: + "#{children(content, tags)}" + in type: 'bulletList', content: + "" + in type: 'orderedList', content: + "
    #{children(content, tags)}
" + in type: 'listItem', content: + "
  • #{children(content, tags)}
  • " + in type: 'text', text:, **rest + if rest[:marks].present? + apply_marks(text, rest[:marks]) + else + text + end + in type: 'mention', attrs: { id: }, **rest + if rest[:marks].present? + apply_marks(tags[id], rest[:marks]) + else + tags[id] + end + 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 +end diff --git a/spec/services/tiptap_service_spec.rb b/spec/services/tiptap_service_spec.rb new file mode 100644 index 000000000..37cdd6d0c --- /dev/null +++ b/spec/services/tiptap_service_spec.rb @@ -0,0 +1,142 @@ +RSpec.describe TiptapService do + describe '.to_html' do + let(:json) do + { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { textAlign: 'right' }, + content: [ + { + type: 'text', + text: 'Hello world!' + } + ] + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Bonjour ', + marks: [{ type: 'italic' }, { type: 'strike' }] + }, + { + type: 'mention', + attrs: { id: 'name' }, + marks: [{ type: 'bold' }, { type: 'underline' }] + }, + { + type: 'text', + text: ' ' + }, + { + type: 'text', + text: '!', + marks: [{ type: 'highlight' }] + } + ] + }, + { + type: 'heading', + attrs: { level: 1 }, + content: [{ type: 'text', text: 'Heading 1' }] + }, + { + type: 'heading', + attrs: { level: 2, textAlign: 'center' }, + content: [{ type: 'text', text: 'Heading 2' }] + }, + { + type: 'heading', + attrs: { level: 3 }, + content: [{ type: 'text', text: 'Heading 3' }] + }, + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Item 1' + } + ] + } + ] + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Item 2' + } + ] + } + ] + } + ] + }, + { + type: 'orderedList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Item 1' + } + ] + } + ] + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Item 2' + } + ] + } + ] + } + ] + } + ] + } + end + let(:tags) { { 'name' => 'Paul' } } + let(:html) do + [ + '

    Hello world!

    ', + '

    Bonjour Paul !

    ', + '

    Heading 1

    ', + '

    Heading 2

    ', + '

    Heading 3

    ', + '', + '
    1. Item 1

    2. Item 2

    ' + ].join + end + + it 'returns html' do + expect(described_class.to_html(json, tags)).to eq(html) + end + end +end