feat(attestation): replace tags in preview for v2
This commit is contained in:
parent
d4c4b3a212
commit
40353fee04
12 changed files with 452 additions and 196 deletions
|
@ -5,8 +5,9 @@ module Administrateurs
|
|||
before_action :retrieve_procedure, :retrieve_attestation_template, :ensure_feature_active
|
||||
|
||||
def show
|
||||
json_body = @attestation_template.json_body&.deep_symbolize_keys
|
||||
@body = TiptapService.new.to_html(json_body, {})
|
||||
preview_dossier = @procedure.dossier_for_preview(current_user)
|
||||
|
||||
@body = @attestation_template.render_attributes_for(dossier: preview_dossier).fetch(:body)
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
|
|
@ -64,26 +64,19 @@ class AttestationTemplate < ApplicationRecord
|
|||
end
|
||||
|
||||
def render_attributes_for(params = {})
|
||||
attributes = {
|
||||
created_at: Time.zone.now,
|
||||
groupe_instructeur = params[:groupe_instructeur]
|
||||
groupe_instructeur ||= params[:dossier]&.groupe_instructeur
|
||||
|
||||
base_attributes = {
|
||||
created_at: Time.current,
|
||||
footer: params.fetch(:footer, footer),
|
||||
logo: params.fetch(:logo, logo.attached? ? logo : nil)
|
||||
signature: signature_to_render(groupe_instructeur)
|
||||
}
|
||||
|
||||
dossier = params[:dossier]
|
||||
|
||||
if dossier.present?
|
||||
attributes.merge({
|
||||
title: replace_tags(title, dossier, escape: false),
|
||||
body: replace_tags(body, dossier, escape: false),
|
||||
signature: signature_to_render(dossier.groupe_instructeur)
|
||||
})
|
||||
if version == 2
|
||||
render_attributes_for_v2(params, base_attributes)
|
||||
else
|
||||
attributes.merge({
|
||||
title: params.fetch(:title, title),
|
||||
body: params.fetch(:body, body),
|
||||
signature: signature_to_render(params[:groupe_instructeur])
|
||||
})
|
||||
render_attributes_for_v1(params, base_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -113,6 +106,48 @@ class AttestationTemplate < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def render_attributes_for_v1(params, base_attributes)
|
||||
attributes = base_attributes.merge(
|
||||
logo: params.fetch(:logo, logo.attached? ? logo : nil)
|
||||
)
|
||||
|
||||
dossier = params[:dossier]
|
||||
|
||||
if dossier.present?
|
||||
attributes.merge(
|
||||
title: replace_tags(title, dossier, escape: false),
|
||||
body: replace_tags(body, dossier, escape: false)
|
||||
)
|
||||
else
|
||||
attributes.merge(
|
||||
title: params.fetch(:title, title),
|
||||
body: params.fetch(:body, body)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def render_attributes_for_v2(params, base_attributes)
|
||||
dossier = params[:dossier]
|
||||
|
||||
json = json_body&.deep_symbolize_keys
|
||||
tiptap = TiptapService.new
|
||||
|
||||
if dossier.present?
|
||||
# 2x faster this way than with `replace_tags` which would reparse text
|
||||
used_tags = tiptap.used_tags_and_libelle_for(json.deep_symbolize_keys)
|
||||
substitutions = tags_substitutions(used_tags, dossier, escape: false)
|
||||
body = tiptap.to_html(json, substitutions)
|
||||
|
||||
attributes.merge(
|
||||
body:
|
||||
)
|
||||
else
|
||||
attributes.merge(
|
||||
body: params.fetch(:body) { tiptap.to_html(json) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def signature_to_render(groupe_instructeur)
|
||||
if groupe_instructeur&.signature&.attached?
|
||||
groupe_instructeur.signature
|
||||
|
|
|
@ -155,14 +155,14 @@ module TagsSubstitutionConcern
|
|||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'individual_first_name',
|
||||
id: 'individual_last_name',
|
||||
libelle: 'nom',
|
||||
description: "nom de l'usager",
|
||||
target: :nom,
|
||||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'individual_last_name',
|
||||
id: 'individual_first_name',
|
||||
libelle: 'prénom',
|
||||
description: "prénom de l'usager",
|
||||
target: :prenom,
|
||||
|
@ -254,6 +254,34 @@ module TagsSubstitutionConcern
|
|||
used_tags_and_libelle_for(text).map { _1.first.nil? ? _1.second : _1.first }
|
||||
end
|
||||
|
||||
def tags_substitutions(tokens, dossier, escape: true)
|
||||
# NOTE:
|
||||
# - tokens est un simple Set d'ids (pas la même structure que dans replace_tags)
|
||||
# - dans replace_tags, on fait référence à des tags avec ou sans id, mais pas ici,
|
||||
# a priori inutile car tiptap ne fait référence qu'aux ids.
|
||||
|
||||
@escape_unsafe_tags = escape
|
||||
|
||||
flat_tags = tags_and_datas_list(dossier).each_with_object({}) do |(tags, data), result|
|
||||
next if data.nil?
|
||||
|
||||
valid_tags = tags_for_dossier_state(tags)
|
||||
|
||||
valid_tags.each do |tag|
|
||||
result[tag[:id]] = [tag, data]
|
||||
end
|
||||
end
|
||||
|
||||
tokens.index_with do |token|
|
||||
case flat_tags[token]
|
||||
in tag, data
|
||||
replace_tag(tag, data)
|
||||
else
|
||||
token
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def format_date(date)
|
||||
|
@ -323,14 +351,7 @@ module TagsSubstitutionConcern
|
|||
|
||||
tokens = parse_tags(text)
|
||||
|
||||
tags_and_datas = [
|
||||
[champ_public_tags(dossier: dossier), dossier.champs_public],
|
||||
[champ_private_tags(dossier: dossier), dossier.champs_private],
|
||||
[dossier_tags, dossier],
|
||||
[ROUTAGE_TAGS, dossier],
|
||||
[INDIVIDUAL_TAGS, dossier.individual],
|
||||
[ENTREPRISE_TAGS, dossier.etablissement&.entreprise]
|
||||
].filter_map do |(tags, data)|
|
||||
tags_and_datas = tags_and_datas_list(dossier).filter_map do |(tags, data)|
|
||||
data && [tags_for_dossier_state(tags).index_by { _1[:id] }, data]
|
||||
end
|
||||
|
||||
|
@ -408,4 +429,15 @@ module TagsSubstitutionConcern
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def tags_and_datas_list(dossier)
|
||||
[
|
||||
[champ_public_tags(dossier:), dossier.champs_public],
|
||||
[champ_private_tags(dossier:), dossier.champs_private],
|
||||
[dossier_tags, dossier],
|
||||
[ROUTAGE_TAGS, dossier],
|
||||
[INDIVIDUAL_TAGS, dossier.individual],
|
||||
[ENTREPRISE_TAGS, dossier.etablissement&.entreprise]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -992,6 +992,14 @@ class Procedure < ApplicationRecord
|
|||
draft_revision.revision_types_de_champ_public.filter { _1.type_de_champ.header_section? }
|
||||
end
|
||||
|
||||
def dossier_for_preview(user)
|
||||
# Try to use a preview or a dossier filled by current user
|
||||
dossiers.where(for_procedure_preview: true).or(dossiers.not_brouillon)
|
||||
.order(Arel.sql("CASE WHEN for_procedure_preview = True THEN 1 ELSE 0 END DESC,
|
||||
CASE WHEN user_id = #{user.id} THEN 1 ELSE 0 END DESC")) \
|
||||
.first
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pieces_jointes_list
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
class TiptapService
|
||||
def to_html(node, tags)
|
||||
def to_html(node, substitutions = {})
|
||||
return '' if node.nil?
|
||||
|
||||
children(node[:content], tags, 0)
|
||||
children(node[:content], substitutions, 0)
|
||||
end
|
||||
|
||||
def used_tags(node, tags = Set.new)
|
||||
case node
|
||||
in type: 'mention', attrs: { id: }
|
||||
tags << id
|
||||
in { content: } if content.is_a?(Array)
|
||||
content.each { used_tags(_1, tags) }
|
||||
else
|
||||
# noop
|
||||
end
|
||||
|
||||
tags
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -11,11 +24,11 @@ class TiptapService
|
|||
@body_started = false
|
||||
end
|
||||
|
||||
def children(content, tags, level)
|
||||
content.map { node_to_html(_1, tags, level) }.join
|
||||
def children(content, substitutions, level)
|
||||
content.map { node_to_html(_1, substitutions, level) }.join
|
||||
end
|
||||
|
||||
def node_to_html(node, tags, level)
|
||||
def node_to_html(node, substitutions, level)
|
||||
if level == 0 && !@body_started && node[:type] == 'paragraph' && node.key?(:content)
|
||||
@body_started = true
|
||||
body_start_mark = " class=\"body-start\""
|
||||
|
@ -23,23 +36,23 @@ class TiptapService
|
|||
|
||||
case node
|
||||
in type: 'header', content:
|
||||
"<header>#{children(content, tags, level + 1)}</header>"
|
||||
"<header>#{children(content, substitutions, level + 1)}</header>"
|
||||
in type: 'footer', content:, **rest
|
||||
"<footer#{text_align(rest[:attrs])}>#{children(content, tags, level + 1)}</footer>"
|
||||
"<footer#{text_align(rest[:attrs])}>#{children(content, substitutions, level + 1)}</footer>"
|
||||
in type: 'headerColumn', content:, **rest
|
||||
"<div#{text_align(rest[:attrs])}>#{children(content, tags, level + 1)}</div>"
|
||||
"<div#{text_align(rest[:attrs])}>#{children(content, substitutions, level + 1)}</div>"
|
||||
in type: 'paragraph', content:, **rest
|
||||
"<p#{body_start_mark}#{text_align(rest[:attrs])}>#{children(content, tags, level + 1)}</p>"
|
||||
"<p#{body_start_mark}#{text_align(rest[:attrs])}>#{children(content, substitutions, level + 1)}</p>"
|
||||
in type: 'title', content:, **rest
|
||||
"<h1#{text_align(rest[:attrs])}>#{children(content, tags, level + 1)}</h1>"
|
||||
"<h1#{text_align(rest[:attrs])}>#{children(content, substitutions, level + 1)}</h1>"
|
||||
in type: 'heading', attrs: { level: hlevel, **attrs }, content:
|
||||
"<h#{hlevel}#{text_align(attrs)}>#{children(content, tags, level + 1)}</h#{hlevel}>"
|
||||
"<h#{hlevel}#{text_align(attrs)}>#{children(content, substitutions, level + 1)}</h#{hlevel}>"
|
||||
in type: 'bulletList', content:
|
||||
"<ul>#{children(content, tags, level + 1)}</ul>"
|
||||
"<ul>#{children(content, substitutions, level + 1)}</ul>"
|
||||
in type: 'orderedList', content:
|
||||
"<ol>#{children(content, tags, level + 1)}</ol>"
|
||||
"<ol>#{children(content, substitutions, level + 1)}</ol>"
|
||||
in type: 'listItem', content:
|
||||
"<li>#{children(content, tags, level + 1)}</li>"
|
||||
"<li>#{children(content, substitutions, level + 1)}</li>"
|
||||
in type: 'text', text:, **rest
|
||||
if rest[:marks].present?
|
||||
apply_marks(text, rest[:marks])
|
||||
|
@ -47,10 +60,12 @@ class TiptapService
|
|||
text
|
||||
end
|
||||
in type: 'mention', attrs: { id: }, **rest
|
||||
text = substitutions.fetch(id) { "--#{id}--" }
|
||||
|
||||
if rest[:marks].present?
|
||||
apply_marks(tags[id], rest[:marks])
|
||||
apply_marks(text, rest[:marks])
|
||||
else
|
||||
tags[id]
|
||||
text
|
||||
end
|
||||
in { type: type } if ["paragraph", "title", "heading"].include?(type) && !node.key?(:content)
|
||||
# noop
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class TagsValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attribute, value)
|
||||
procedure = record.procedure
|
||||
tags = record.used_type_de_champ_tags(value || '')
|
||||
tags = record.used_type_de_champ_tags(value.to_s)
|
||||
|
||||
invalid_tags = tags.filter_map do |(tag, stable_id)|
|
||||
tag if stable_id.nil?
|
||||
|
|
|
@ -102,6 +102,7 @@ namespace :benchmarks do
|
|||
controller.request = ActionDispatch::TestRequest.create
|
||||
controller.response = ActionDispatch::TestResponse.new
|
||||
controller.request.env['warden'] = warden
|
||||
# controller.request.path_parameters[:format] = 'pdf'
|
||||
|
||||
params = ENV.fetch("PARAMS") { "" }.split(",")
|
||||
params.each do |param|
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
describe Administrateurs::AttestationTemplateV2sController, type: :controller do
|
||||
let(:admin) { create(:administrateur) }
|
||||
let(:attestation_template) { build(:attestation_template, :v2) }
|
||||
let!(:procedure) { create(:procedure, administrateur: admin, attestation_template: attestation_template, libelle: "Ma démarche") }
|
||||
let(:logo) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') }
|
||||
let(:signature) { fixture_file_upload('spec/fixtures/files/black.png', 'image/png') }
|
||||
|
||||
before do
|
||||
sign_in(admin.user)
|
||||
Flipper.enable(:attestation_v2)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
subject do
|
||||
get :show, params: { procedure_id: procedure.id }
|
||||
response.body
|
||||
end
|
||||
|
||||
context 'if an attestation template exists on the procedure' do
|
||||
render_views
|
||||
|
||||
context 'with preview dossier' do
|
||||
let!(:dossier) { create(:dossier, :en_construction, procedure:, for_procedure_preview: true) }
|
||||
|
||||
it do
|
||||
is_expected.to include("Mon titre pour Ma démarche")
|
||||
is_expected.to include("n° #{dossier.id}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'without preview dossier' do
|
||||
it do
|
||||
is_expected.to include("Mon titre pour --dossier_procedure_libelle--")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with logo label' do
|
||||
it do
|
||||
is_expected.to include("Ministère des devs")
|
||||
is_expected.to match(/centered_marianne-\w+\.svg/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with label direction' do
|
||||
let(:attestation_template) { build(:attestation_template, :v2, label_direction: "calé à droite") }
|
||||
|
||||
it do
|
||||
is_expected.to include("calé à droite")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with footer' do
|
||||
let(:attestation_template) { build(:attestation_template, :v2, footer: "c'est le pied") }
|
||||
|
||||
it do
|
||||
is_expected.to include("c'est le pied")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with additional logo' do
|
||||
let(:attestation_template) { build(:attestation_template, :v2, logo:) }
|
||||
|
||||
it do
|
||||
is_expected.to include("Ministère des devs")
|
||||
is_expected.to include("white.png")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with signature' do
|
||||
let(:attestation_template) { build(:attestation_template, :v2, signature:) }
|
||||
|
||||
it do
|
||||
is_expected.to include("black.png")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,11 +2,46 @@ FactoryBot.define do
|
|||
factory :attestation_template do
|
||||
title { 'title' }
|
||||
body { 'body' }
|
||||
json_body { nil }
|
||||
footer { 'footer' }
|
||||
activated { true }
|
||||
version { 1 }
|
||||
official_layout { true }
|
||||
label_direction { nil }
|
||||
label_logo { nil }
|
||||
association :procedure
|
||||
end
|
||||
|
||||
trait :v2 do
|
||||
version { 2 }
|
||||
body { nil }
|
||||
title { nil }
|
||||
label_logo { "Ministère des devs" }
|
||||
|
||||
json_body do
|
||||
{
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{
|
||||
"type" => "header", "content" => [
|
||||
{ "type" => "headerColumn", "attrs" => { "textAlign" => "left" }, "content" => [{ "type" => "paragraph", "attrs" => { "textAlign" => "left" } }] },
|
||||
{ "type" => "headerColumn", "attrs" => { "textAlign" => "left" }, "content" => [{ "type" => "paragraph", "attrs" => { "textAlign" => "left" } }] }
|
||||
]
|
||||
},
|
||||
{ "type" => "title", "attrs" => { "textAlign" => "center" }, "content" => [{ "text" => "Mon titre pour ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_procedure_libelle", "label" => "libellé démarche" } }] },
|
||||
{ "type" => "paragraph", "attrs" => { "textAlign" => "left" }, "content" => [{ "text" => "Dossier: n° ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] },
|
||||
{
|
||||
"type" => "paragraph",
|
||||
"content" => [
|
||||
{ "text" => "Nom: ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "individual_last_name", "label" => "prénom" } }, { "text" => " ", "type" => "text" },
|
||||
{ "type" => "mention", "attrs" => { "id" => "individual_first_name", "label" => "nom" } }, { "text" => " ", "type" => "text" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_files do
|
||||
logo { Rack::Test::UploadedFile.new('spec/fixtures/files/logo_test_procedure.png', 'image/png') }
|
||||
signature { Rack::Test::UploadedFile.new('spec/fixtures/files/logo_test_procedure.png', 'image/png') }
|
||||
|
|
|
@ -173,5 +173,16 @@ describe AttestationTemplate, type: :model do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'body v2' do
|
||||
let(:attestation) { create(:attestation_template, :v2) }
|
||||
let(:dossier) { create(:dossier, procedure: attestation.procedure, individual: build(:individual, nom: 'Doe', prenom: 'John')) }
|
||||
|
||||
it do
|
||||
body = attestation.render_attributes_for(dossier: dossier)[:body]
|
||||
expect(body).to include("Mon titre pour #{dossier.procedure.libelle}")
|
||||
expect(body).to include("Doe John")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,6 +32,39 @@ describe TagsSubstitutionConcern, type: :model do
|
|||
end).new(procedure, state)
|
||||
end
|
||||
|
||||
describe 'tags_substitutions' do
|
||||
let(:individual) { nil }
|
||||
let(:etablissement) { create(:etablissement) }
|
||||
let(:dossier) { create(:dossier, :en_construction, procedure:, individual:, etablissement:) }
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let(:tags) { Set.new(["dossier_number"]) }
|
||||
|
||||
subject { template_concern.tags_substitutions(tags, dossier) }
|
||||
|
||||
context 'dossiers metadata' do
|
||||
before { travel_to(Time.zone.local(2024, 1, 15, 12)) }
|
||||
let(:tags) { Set.new(["dossier_number", "dossier_depose_at", "dossier_processed_at", "dossier_procedure_libelle"]) }
|
||||
|
||||
it do
|
||||
is_expected.to eq(
|
||||
"dossier_number" => dossier.id.to_s,
|
||||
"dossier_depose_at" => "15/01/2024",
|
||||
"dossier_processed_at" => "",
|
||||
"dossier_procedure_libelle" => procedure.libelle
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the dossier and the procedure has an individual' do
|
||||
let(:for_individual) { true }
|
||||
let(:individual) { Individual.create(nom: 'Adama', prenom: 'William', gender: 'M') }
|
||||
|
||||
let(:tags) { Set.new(['individual_gender', 'individual_last_name']) }
|
||||
|
||||
it { is_expected.to eq({ "individual_gender" => 'M', "individual_last_name" => "Adama" }) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'replace_tags' do
|
||||
let(:individual) { nil }
|
||||
let(:etablissement) { create(:etablissement) }
|
||||
|
|
|
@ -1,155 +1,156 @@
|
|||
RSpec.describe TiptapService do
|
||||
let(:json) do
|
||||
{
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'header',
|
||||
content: [
|
||||
{
|
||||
type: 'headerColumn',
|
||||
content: [{ type: 'text', text: 'Left' }]
|
||||
},
|
||||
{
|
||||
type: 'headerColumn',
|
||||
content: [{ type: 'text', text: 'Right' }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
content: [{ type: 'text', text: 'Title' }]
|
||||
},
|
||||
{
|
||||
type: 'title' # remained empty in editor
|
||||
},
|
||||
{
|
||||
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: 'heading',
|
||||
attrs: { level: 3 } # remained empty in editor
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: { textAlign: 'right' },
|
||||
content: [{ type: 'text', text: 'First paragraph' }]
|
||||
},
|
||||
{
|
||||
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: 'paragraph'
|
||||
# no content, empty line
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'footer',
|
||||
content: [{ type: 'text', text: 'Footer' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
describe '.to_html' do
|
||||
let(:json) do
|
||||
{
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'header',
|
||||
content: [
|
||||
{
|
||||
type: 'headerColumn',
|
||||
content: [{ type: 'text', text: 'Left' }]
|
||||
},
|
||||
{
|
||||
type: 'headerColumn',
|
||||
content: [{ type: 'text', text: 'Right' }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
content: [{ type: 'text', text: 'Title' }]
|
||||
},
|
||||
{
|
||||
type: 'title' # remained empty in editor
|
||||
},
|
||||
{
|
||||
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: 'heading',
|
||||
attrs: { level: 3 } # remained empty in editor
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: { textAlign: 'right' },
|
||||
content: [{ type: 'text', text: 'First paragraph' }]
|
||||
},
|
||||
{
|
||||
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: 'paragraph'
|
||||
# no content, empty line
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'footer',
|
||||
content: [{ type: 'text', text: 'Footer' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
let(:tags) { { 'name' => 'Paul' } }
|
||||
let(:substitutions) { { 'name' => 'Paul' } }
|
||||
let(:html) do
|
||||
[
|
||||
'<header><div>Left</div><div>Right</div></header>',
|
||||
|
@ -166,7 +167,13 @@ RSpec.describe TiptapService do
|
|||
end
|
||||
|
||||
it 'returns html' do
|
||||
expect(described_class.new.to_html(json, tags)).to eq(html)
|
||||
expect(described_class.new.to_html(json, substitutions)).to eq(html)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#used_tags' do
|
||||
it 'returns used tags' do
|
||||
expect(described_class.new.used_tags(json)).to eq(Set.new(['name']))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue