From 40f5a4612e9b66495efb2692600999a35a6a9196 Mon Sep 17 00:00:00 2001
From: gloas <gloas@afi-sa.fr>
Date: Mon, 19 Sep 2022 14:24:12 +0200
Subject: [PATCH] dev #162170 split record main title with html for views

---
 FEATURES/162170                               | 10 +++
 VERSIONS_WIP/162170                           |  1 +
 .../views/scripts/abonne/delete-review.phtml  |  2 +-
 .../abonne/supprimer-de-la-selection.phtml    |  2 +-
 .../AlbumCategorie/EadGallicaVisitor.php      |  2 +-
 library/Class/Notice.php                      | 11 ++-
 library/Class/Notice/Titles.php               | 86 ++++++++++++-------
 library/Class/Profil.php                      |  2 +-
 .../View/Helper/Template/CardHelper.php       |  4 +-
 .../View/Helper/Template/LayoutAccordion.php  |  2 +-
 .../Template/RenderAuthorDescription.php      |  2 +-
 .../Helper/Template/TitleAndSubtitles.php     | 32 +++++++
 .../Library/View/Wrapper/Abstract.php         | 17 +++-
 .../Library/View/Wrapper/ActivitySession.php  |  6 +-
 .../Intonation/Library/View/Wrapper/Card.php  |  2 +-
 .../Library/View/Wrapper/Record.php           | 27 +++---
 .../Library/View/Wrapper/RendezVous.php       |  8 +-
 .../Library/View/Wrapper/Review.php           |  2 +-
 .../Library/View/Wrapper/ReviewsByRecord.php  |  2 +-
 .../Library/View/Wrapper/RssItem.php          |  4 +-
 .../Library/View/Wrapper/Search.php           |  2 +-
 .../Intonation/Library/View/Wrapper/Work.php  |  4 +-
 .../Intonation/View/Jumbotron/Abstract.php    |  2 +-
 .../Intonation/View/RenderTimeline.php        |  2 +-
 public/opac/css/core.css                      |  4 +
 tests/library/Class/NoticeTest.php            | 10 ++-
 .../IndexablePagesSearchTest.php              |  2 +-
 .../scenarios/Serials/SerialsDetailsTest.php  |  3 +-
 .../ShelfNavigation/ShelfNavigationTest.php   |  6 +-
 .../Templates/TemplatesRecordsTest.php        | 16 +++-
 .../Templates/TemplatesSearchTest.php         |  2 +-
 .../Templates/TemplatesTimelineWidgetTest.php |  5 +-
 .../Templates/TemplatesWidgetCarouselTest.php |  6 +-
 33 files changed, 203 insertions(+), 85 deletions(-)
 create mode 100644 FEATURES/162170
 create mode 100644 VERSIONS_WIP/162170
 create mode 100644 library/ZendAfi/View/Helper/Template/TitleAndSubtitles.php

diff --git a/FEATURES/162170 b/FEATURES/162170
new file mode 100644
index 00000000000..f127ee54525
--- /dev/null
+++ b/FEATURES/162170
@@ -0,0 +1,10 @@
+        '162170' =>
+            ['Label' => $this->_('ENSA : Modif affichage titre chapeau'),
+             'Desc' => '',
+             'Image' => '',
+             'Video' => '',
+             'Category' => '',
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => '',
+             'Test' => '',
+             'Date' => '2022-09-19'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/162170 b/VERSIONS_WIP/162170
new file mode 100644
index 00000000000..09bc6d97cc0
--- /dev/null
+++ b/VERSIONS_WIP/162170
@@ -0,0 +1 @@
+ - fonctionnalité #162170 : ENSA : Modif affichage titre chapeau
\ No newline at end of file
diff --git a/application/modules/opac/views/scripts/abonne/delete-review.phtml b/application/modules/opac/views/scripts/abonne/delete-review.phtml
index 710a1194e7e..8f858447910 100644
--- a/application/modules/opac/views/scripts/abonne/delete-review.phtml
+++ b/application/modules/opac/views/scripts/abonne/delete-review.phtml
@@ -5,7 +5,7 @@ $wrapper = (new Intonation_Library_View_Wrapper_Review)
 $html = [$this->div(['class' => 'col-10'],
                     $this->tag('h3',
                                $this->_('Supprimer l\'avis %s',
-                                        $wrapper->getMainTitle()),
+                                        $wrapper->getMainTitleAsText()),
                                ['class' => 'pt-2 mx-2 border-top border-danger'])),
 
          $this->div(['class' => 'col-10'],
diff --git a/application/modules/opac/views/scripts/abonne/supprimer-de-la-selection.phtml b/application/modules/opac/views/scripts/abonne/supprimer-de-la-selection.phtml
index 924e2f59f80..53d4b8e7f60 100644
--- a/application/modules/opac/views/scripts/abonne/supprimer-de-la-selection.phtml
+++ b/application/modules/opac/views/scripts/abonne/supprimer-de-la-selection.phtml
@@ -5,7 +5,7 @@ $wrapper = (new Intonation_Library_View_Wrapper_Record)
 $html = [$this->div(['class' => 'col-10'],
                     $this->tag('h3',
                                $this->_('Retirer le document %s de la sélection %s',
-                                        $wrapper->getMainTitle(),
+                                        $wrapper->getMainTitleAsText(),
                                         $this->selection->getLibelle()),
                                ['class' => 'pt-2 mx-2 border-top border-danger'])),
 
diff --git a/library/Class/AlbumCategorie/EadGallicaVisitor.php b/library/Class/AlbumCategorie/EadGallicaVisitor.php
index 9e896702b63..511e0c6e879 100644
--- a/library/Class/AlbumCategorie/EadGallicaVisitor.php
+++ b/library/Class/AlbumCategorie/EadGallicaVisitor.php
@@ -107,7 +107,7 @@ class Class_AlbumCategorie_EadGallicaVisitor extends Class_AlbumCategorie_EadVis
     $b = $this->_builder;
     $titre = $album->getTitre();
     $titre .= $album->getSousTitre()
-      ? ' : ' . $album->getSousTitre()
+      ? ' ' . $album->getSousTitre()
       : '';
     return $b->unittitle($b->cdata($titre));
   }
diff --git a/library/Class/Notice.php b/library/Class/Notice.php
index 62274e894e5..af22bea9842 100644
--- a/library/Class/Notice.php
+++ b/library/Class/Notice.php
@@ -785,15 +785,15 @@ class Class_Notice extends Storm_Model_Abstract {
   }
 
 
-  public function getRecordTitle() {
+  public function getRecordTitle() : string {
     return $this->_getDataMap()->getMainTitle();
   }
 
 
-  public function getTitrePrincipal($separator = BR) {
+  public function getTitrePrincipal($separator = BR) : string {
     return $this->_getTitles()
       ->getAllTitles($separator,
-                     $this->isAuthority() ? $this->getRecordTitle() : null);
+                     $this->isAuthority() ? $this->getRecordTitle() : '');
   }
 
 
@@ -816,6 +816,11 @@ class Class_Notice extends Storm_Model_Abstract {
   }
 
 
+  public function getTitleAndSubtitlesAsArray() : array {
+    return $this->_getTitles()->getAllTitlesAndSubtitlesAsArray($this->isAuthority() ? $this->getRecordTitle() : '');
+  }
+
+
   public function getSubtitle() {
     return $this->_getTitles()->getSubtitle();
   }
diff --git a/library/Class/Notice/Titles.php b/library/Class/Notice/Titles.php
index 26610f12154..147378f390c 100644
--- a/library/Class/Notice/Titles.php
+++ b/library/Class/Notice/Titles.php
@@ -58,38 +58,75 @@ class Class_Notice_Titles {
 
 
   public function getAllTitles(string $separator = BR,
-                               ?string $main_title = null) : string {
+                               string $main_title = '') : string {
+    return implode($separator, $this->getAllTitlesAsArray($main_title));
+  }
+
+
+  public function getAllTitlesAsArray(string $main_title = '') : array {
     $record_title = $main_title
       ? $this->_filter($main_title)
       : $this->_getFirstSubfield(static::MAIN_TITLE);
 
-    $titles = [];
-    $volumes = [];
-    foreach($this->_unimarc->get_subfield(461, 't', 'v') as $title_volume) {
-      $title = $this->_filter($title_volume['t']);
-      $titles []= $title ? $title : $record_title;
-      $volumes []= $title_volume['v'];
+    $titles_as_array = [];
+
+    foreach($this->_unimarc->get_subfield(461, 't', 'v') as $title_volume)
+      $titles_as_array = $this->_add461TextTo($this->_filter($title_volume['t']),
+                                              $this->_filter($title_volume['v']),
+                                              $record_title,
+                                              $titles_as_array);
+
+    if ( ! isset($titles_as_array[static::MAIN_TITLE]))
+      $titles_as_array[static::MAIN_TITLE] = $record_title;
+
+    return array_filter($titles_as_array);
+  }
+
+
+  protected function _add461TextTo(string $title, string $volume, string $record_title, array $titles) : array {
+    if ( ! $title && ! $volume)
+      return $titles;
+
+    $key = static::SET_TITLE . ' ' . static::SET_TITLE . '_' . (count($titles) + 1);
+
+    if ( $title && $volume && $title !== $record_title) {
+      $titles [$key] = $this->_('%s n° %s', $title, $volume);
+      return $titles;
     }
 
-    if (!in_array($record_title, $titles)) {
-      $titles []= $record_title;
-      $volumes []= '';
+    if ( $title && ! $volume && $title !== $record_title ) {
+      $titles [$key] = $title;
+      return $titles;
     }
 
-    $titles_volumes = [];
-    foreach($titles as $i => $title)
-      $titles_volumes []= implode($this->_(' n° '),
-                                  array_filter([$title, $volumes[$i]]));
+    if ( $volume )
+      $titles [static::MAIN_TITLE] = $this->_('%s n° %s', $record_title, $volume);
+
+    return $titles;
+}
+
+
+  public function getAllTitlesAndSubtitlesAsArray() : array {
+    $titles = $this->getAllTitlesAsArray();
 
-    return implode($separator,
-                   array_filter($titles_volumes));
+    return ($subtitles = $this->_getSubtitlesAsArray())
+      ? array_merge($titles, $subtitles)
+      : $titles;
   }
 
 
-  public function getSubtitle() : string {
-    $zones = Class_Profil::getCurrentProfil()->getZonesTitre();
+  protected function _getSubtitlesAsArray() : array {
+    $subtitles = [];
+    foreach(Class_Profil::getCurrentProfil()->getZonesTitre() as $zone)
+      if ($subtitle = $this->_getFirstSubfield($zone))
+        $subtitles [$zone] = ': ' . $subtitle;
 
-    return implode(' : ', $this->_getFirstTitlesInZones($zones));
+    return $subtitles;
+  }
+
+
+  public function getSubtitle() : string {
+    return implode($this->_getSubtitlesAsArray());
   }
 
 
@@ -175,17 +212,6 @@ class Class_Notice_Titles {
   }
 
 
-  protected function _getFirstTitlesInZones(array $zones) : array {
-    $titres = new Storm_Collection;
-    foreach ($zones as $field_description)
-      $titres->add($this->_getFirstSubfield($field_description));
-
-    return $titres
-      ->select(fn($value) => $value)
-      ->getArrayCopy();
-  }
-
-
   protected function _getFirstSubfield(string $field_description) : string {
     return ($values = $this->_getSubfield($field_description))
       ? $this->_filter(reset($values))
diff --git a/library/Class/Profil.php b/library/Class/Profil.php
index c2034d915d7..94844a92f49 100644
--- a/library/Class/Profil.php
+++ b/library/Class/Profil.php
@@ -2324,7 +2324,7 @@ class Class_Profil extends Storm_Model_Abstract {
 
 
   /** @return array la liste des zones titre a afficher dans le resultat de recherche */
-  public function getZonesTitre() {
+  public function getZonesTitre() : array {
     $cfg = $this->getCfgModulesAsArray();
 
     $zones = isset($cfg['recherche']['resultatsimple']['zones_titre'])
diff --git a/library/ZendAfi/View/Helper/Template/CardHelper.php b/library/ZendAfi/View/Helper/Template/CardHelper.php
index 3a0b6bf5210..7fd6c7957aa 100644
--- a/library/ZendAfi/View/Helper/Template/CardHelper.php
+++ b/library/ZendAfi/View/Helper/Template/CardHelper.php
@@ -49,8 +49,8 @@ abstract class ZendAfi_View_Helper_Template_CardHelper extends ZendAfi_View_Help
       $attribs['aria-label'] = $aria_label;
 
     return $this->view->tagAnchor($link->getUrl(),
-                           $main_title,
-                           $attribs
+                                  $main_title,
+                                  $attribs
     );
   }
 }
diff --git a/library/ZendAfi/View/Helper/Template/LayoutAccordion.php b/library/ZendAfi/View/Helper/Template/LayoutAccordion.php
index 4c3c2fb9ce5..70114193bcf 100644
--- a/library/ZendAfi/View/Helper/Template/LayoutAccordion.php
+++ b/library/ZendAfi/View/Helper/Template/LayoutAccordion.php
@@ -97,7 +97,7 @@ class Intonation_View_RenderAccordionCarousel_Content extends ZendAfi_View_Helpe
                         'id' => $this->_heading_id ],
                        $this->_tag('h2',
                                    $this->_tag('button',
-                                               $this->_element->getMainTitle(),
+                                               $this->_element->getMainTitleAsText(),
                                                ['class' => 'accordion_button' . $class,
                                                 'type' => 'button',
                                                 'data-toggle' => 'collapse',
diff --git a/library/ZendAfi/View/Helper/Template/RenderAuthorDescription.php b/library/ZendAfi/View/Helper/Template/RenderAuthorDescription.php
index 6ab70bc9644..fe76f9e4c70 100644
--- a/library/ZendAfi/View/Helper/Template/RenderAuthorDescription.php
+++ b/library/ZendAfi/View/Helper/Template/RenderAuthorDescription.php
@@ -38,6 +38,6 @@ class ZendAfi_View_Helper_Template_RenderAuthorDescription
 
 
   protected function _getPageTitle($wrapped) {
-    return $this->_('Page auteur %s', $wrapped->getMainTitle());
+    return $this->_('Page auteur %s', $wrapped->getMainTitleAsText());
   }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Template/TitleAndSubtitles.php b/library/ZendAfi/View/Helper/Template/TitleAndSubtitles.php
new file mode 100644
index 00000000000..be95bc9f833
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Template/TitleAndSubtitles.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ZendAfi_View_Helper_Template_TitleAndSubtitles extends ZendAfi_View_Helper_BaseHelper {
+  public function titleAndSubtitles(Class_Notice $record) : string {
+    $html = [];
+
+    foreach($record->getTitleAndSubtitlesAsArray() as $class => $title)
+      $html [] = $this->_tag('span', $title, ['class' => $class]);
+
+    return implode($this->_tag('span', ' ', ['class' => 'text_whitespace']), $html);
+  }
+}
diff --git a/library/templates/Intonation/Library/View/Wrapper/Abstract.php b/library/templates/Intonation/Library/View/Wrapper/Abstract.php
index a5c60375133..bb8412d754e 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Abstract.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Abstract.php
@@ -33,7 +33,9 @@ abstract class Intonation_Library_View_Wrapper_Abstract {
     $_widget_context,
     $_widget_context_params = [],
     $_rich_content,
-    $_in_js_search = false;
+    $_in_js_search = false,
+    $_main_title_as_text_cache = null;
+
 
   public function __sleep() {
     if ($this->_model) {
@@ -160,7 +162,7 @@ abstract class Intonation_Library_View_Wrapper_Abstract {
 
 
   public function formatForSearch() {
-    return implode(' ', [$this->getMainTitle(),
+    return implode(' ', [$this->getMainTitleAsText(),
                          $this->getSecondaryTitle(),
                          $this->getDescription()
                          ]);
@@ -174,7 +176,7 @@ abstract class Intonation_Library_View_Wrapper_Abstract {
 
     return $this->_search_content = $this->_in_js_search
       ? $this->_view->hideContentForJS(implode(' ',
-                                               [$this->getMainTitle(),
+                                               [$this->getMainTitleAsText(),
                                                 $this->getSecondaryTitle(),
                                                 $this->getDescription(),
                                                 '%s']))
@@ -233,7 +235,14 @@ abstract class Intonation_Library_View_Wrapper_Abstract {
 
   public function getPictureAlt() {
     return $this->_('Couverture de %s',
-                    strip_tags($this->getMainTitle()));
+                    $this->getMainTitleAsText());
+  }
+
+
+  public function getMainTitleAsText() : string {
+    return isset($this->_main_title_as_text_cache)
+      ? $this->_main_title_as_text_cache
+      : ($this->_main_title_as_text_cache = strip_tags($this->getMainTitle()));
   }
 
 
diff --git a/library/templates/Intonation/Library/View/Wrapper/ActivitySession.php b/library/templates/Intonation/Library/View/Wrapper/ActivitySession.php
index a598f043a8d..f68da8a37f8 100644
--- a/library/templates/Intonation/Library/View/Wrapper/ActivitySession.php
+++ b/library/templates/Intonation/Library/View/Wrapper/ActivitySession.php
@@ -41,7 +41,7 @@ class Intonation_Library_View_Wrapper_ActivitySession
                                    null, true))
         ->setText($this->_('En savoir plus'))
         ->setImage($this->getIco('read-document', 'library'))
-        ->setTitle($this->_('En savoir plus sur %s.', $this->getMainTitle()));
+        ->setTitle($this->_('En savoir plus sur %s.', $this->getMainTitleAsText()));
   }
 
 
@@ -74,7 +74,7 @@ class Intonation_Library_View_Wrapper_ActivitySession
                                            null, true))
                 ->setImage($this->getIco('add', 'utils'))
                 ->setText($this->_('S\'inscrire'))
-                ->setTitle($this->_('S\'inscrire à l\'activité %s.', $this->getMainTitle()))];
+                ->setTitle($this->_('S\'inscrire à l\'activité %s.', $this->getMainTitleAsText()))];
 
     if ($link = Intonation_Library_Link_AdminSessionActivityRegistrations::newFor($this->_model,
                                                                                   $this->_view))
@@ -102,7 +102,7 @@ class Intonation_Library_View_Wrapper_ActivitySession
 
 
   public function getBadges() {
-    $main_title = $this->getMainTitle();
+    $main_title = $this->getMainTitleAsText();
 
     $badges = [
                ((new Intonation_Library_Badge)
diff --git a/library/templates/Intonation/Library/View/Wrapper/Card.php b/library/templates/Intonation/Library/View/Wrapper/Card.php
index 86ce58578ba..1d493fd6731 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Card.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Card.php
@@ -38,7 +38,7 @@ class Intonation_Library_View_Wrapper_Card extends Intonation_Library_View_Wrapp
                                         'id' => $this->_model->getId()]))
             ->setImage($this->getIco('unlink_user', 'utils'))
             ->setText($this->_('Supprimer'))
-            ->setTitle($this->_('Ne plus gérer la carte de %s', $this->getMainTitle()))
+            ->setTitle($this->_('Ne plus gérer la carte de %s', $this->getMainTitleAsText()))
             ->setClass('text-danger')
     ];
   }
diff --git a/library/templates/Intonation/Library/View/Wrapper/Record.php b/library/templates/Intonation/Library/View/Wrapper/Record.php
index 3113823a974..ada78cca55c 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Record.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Record.php
@@ -33,9 +33,16 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
     $_decorate_label_callback;
 
 
-  public function getMainTitle() {
+  public function getMainTitle() : string {
     return $this->_main_title = $this->_main_title
       ? $this->_main_title
+      : $this->_getMainTitle();
+  }
+
+
+  protected function _getMainTitle() : string {
+    return $this->_view
+      ? $this->_view->titleAndSubtitles($this->_model)
       : $this->_model->getTitreEtSousTitre(' ');
   }
 
@@ -63,10 +70,10 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
       ->setImage($this->getIco('read-document', 'library'))
       ->setText($this->_('Voir'))
       ->setScreenReaderText($this->_(' le document %s de %s de type %s',
-                                     $this->getMainTitle(),
+                                     $this->getMainTitleAsText(),
                                      $this->getSecondaryTitle(),
                                      $this->getDocTypeLabel()))
-      ->setTitle($this->_('Voir le document %s', $this->getMainTitle()))
+      ->setTitle($this->_('Voir le document %s', $this->getMainTitleAsText()))
       ->setClass('read_document');
   }
 
@@ -119,7 +126,7 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
                                     'action' => 'thumbnail',
                                     'id' => $this->_model->getId()], null, true))
         ->setImage($this->getIco('edit', 'utils'))
-        ->setTitle($this->_('Modifier la vignette de %s', $this->_model->getTitrePrincipal(' ')))
+        ->setTitle($this->_('Modifier la vignette de %s', $this->getMainTitleAsText()))
         ->setClass('menu_admin_front_anchor record_change_thumbnail')
         ->setPopup('true');
   }
@@ -151,7 +158,7 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
 
 
   public function getDescriptionTitle() {
-    return $this->_('Résumé du document %s', $this->getMainTitle());
+    return $this->_('Résumé du document %s', $this->getMainTitleAsText());
   }
 
 
@@ -209,7 +216,7 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
                                               'page' => null]))
                   ->setText($this->_model->isNouveaute() ? $this->_('Nouveauté') : '')
                   ->setTitle($this->_('Le document %s est nouveau dans votre bibliothèque',
-                                      $this->_model->getTitrePrincipal(' '))));
+                                      $this->getMainTitleAsText())));
 
     $badges = $this->_addCollectionBadges($badges);
 
@@ -251,7 +258,7 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
         ->setText($year)
         ->setTitle($this->_('Afficher tous les documents ayant la date d\'édition : %d, commele document %s ',
                             $year,
-                            $this->_model->getTitrePrincipal(' ')));
+                            $this->getMainTitleAsText()));
 
     return $badges;
   }
@@ -524,7 +531,7 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
         ->setUrl($sso->renderUrl($this->_view))
         ->setImage($this->getIco('play', 'utils'))
         ->setText($this->_('Consulter en ligne'))
-        ->setTitle($this->_('Consulter le document %s en ligne', $this->getMainTitle()))
+        ->setTitle($this->_('Consulter le document %s en ligne', $this->getMainTitleAsText()))
         ->setClass('view_online_resource');
 
       return $actions;
@@ -541,7 +548,7 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
         ->setUrl($url)
         ->setImage($this->getIco('play', 'utils'))
         ->setText($this->_('Description en ligne'))
-        ->setTitle($this->_('Description de %s en ligne', $this->getMainTitle()))
+        ->setTitle($this->_('Description de %s en ligne', $this->getMainTitleAsText()))
         ->setClass('view_online_resource_description');
 
       return $actions;
@@ -571,7 +578,7 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
                                   'record_id' => $this->_model->getId()]))
       ->setImage($this->getIco('hold', 'library'))
       ->setText($this->_('Réserver'))
-      ->setTitle($this->_('Réserver le document %s', $this->getMainTitle()))
+      ->setTitle($this->_('Réserver le document %s', $this->getMainTitleAsText()))
       ->setClass('hold_record')
       ->setPopup(true);
 
diff --git a/library/templates/Intonation/Library/View/Wrapper/RendezVous.php b/library/templates/Intonation/Library/View/Wrapper/RendezVous.php
index fba56362963..161ce9f7a82 100644
--- a/library/templates/Intonation/Library/View/Wrapper/RendezVous.php
+++ b/library/templates/Intonation/Library/View/Wrapper/RendezVous.php
@@ -74,23 +74,23 @@ class Intonation_Library_View_Wrapper_RendezVous extends Intonation_Library_View
                (new Intonation_Library_Badge)
                ->setText($this->_model->getFormattedDate())
                ->setClass('badge-info')
-               ->setTitle($this->_('Date du rendez-vous %s', $this->getMainTitle())),
+               ->setTitle($this->_('Date du rendez-vous %s', $this->getMainTitleAsText())),
 
                (new Intonation_Library_Badge)
                ->setText($this->_model->getFormattedBeginTime())
                ->setClass('badge-warning')
-               ->setTitle($this->_('Heure de début du rendez-vous %s', $this->getMainTitle())),
+               ->setTitle($this->_('Heure de début du rendez-vous %s', $this->getMainTitleAsText())),
 
                (new Intonation_Library_Badge)
                ->setText($this->_model->getFormattedEndTime())
                ->setClass('badge-warning')
-               ->setTitle($this->_('Heure de fin du rendez-vous %s', $this->getMainTitle()))
+               ->setTitle($this->_('Heure de fin du rendez-vous %s', $this->getMainTitleAsText()))
     ];
 
     $location = (new Intonation_Library_Badge)
         ->setText($this->_model->getLocationLabel())
         ->setClass('badge-primary')
-        ->setTitle($this->_('Lieu du rendez-vous %s', $this->getMainTitle()));
+        ->setTitle($this->_('Lieu du rendez-vous %s', $this->getMainTitleAsText()));
 
     if ($library = $this->_model->getLibrary())
       $location
diff --git a/library/templates/Intonation/Library/View/Wrapper/Review.php b/library/templates/Intonation/Library/View/Wrapper/Review.php
index f69cf4b7bd2..1a31077a6b2 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Review.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Review.php
@@ -132,7 +132,7 @@ class Intonation_Library_View_Wrapper_Review extends Intonation_Library_View_Wra
 
 
   public function getDescriptionTitle() {
-    return $this->_('Résumé de l\'avis %s', $this->getMainTitle());
+    return $this->_('Résumé de l\'avis %s', $this->getMainTitleAsText());
   }
 
 
diff --git a/library/templates/Intonation/Library/View/Wrapper/ReviewsByRecord.php b/library/templates/Intonation/Library/View/Wrapper/ReviewsByRecord.php
index b4fac458cd2..26ea74de537 100644
--- a/library/templates/Intonation/Library/View/Wrapper/ReviewsByRecord.php
+++ b/library/templates/Intonation/Library/View/Wrapper/ReviewsByRecord.php
@@ -79,7 +79,7 @@ class Intonation_Library_View_Wrapper_ReviewsByRecord extends Intonation_Library
       ->setUrl($nav_url)
       ->setImage($this->getIco('read-review', 'library'))
       ->setText($this->_('Lire les avis'))
-      ->setTitle($this->_('Lire les avis sur %s', $this->getMainTitle()));
+      ->setTitle($this->_('Lire les avis sur %s', $this->getMainTitleAsText()));
   }
 
 
diff --git a/library/templates/Intonation/Library/View/Wrapper/RssItem.php b/library/templates/Intonation/Library/View/Wrapper/RssItem.php
index 05a4140d09f..618acfbe4a8 100644
--- a/library/templates/Intonation/Library/View/Wrapper/RssItem.php
+++ b/library/templates/Intonation/Library/View/Wrapper/RssItem.php
@@ -68,7 +68,7 @@ class Intonation_Library_View_Wrapper_RssItem extends Intonation_Library_View_Wr
       ->setUrl($this->_getLink())
       ->setImage($this->getIco('read-document', 'library'))
       ->setText($this->_('Accéder au contenu'))
-      ->setTitle($this->_('Afficher le  contenu de %s', $this->getMainTitle()));
+      ->setTitle($this->_('Afficher le  contenu de %s', $this->getMainTitleAsText()));
   }
 
 
@@ -129,7 +129,7 @@ class Intonation_Library_View_Wrapper_RssItem extends Intonation_Library_View_Wr
                ->setTag('span')
                ->setClass('badge-info')
                ->setText($this->_getPubDate())
-               ->setTitle($this->_('Date de diffusion de %s', $this->getMainTitle()))];
+               ->setTitle($this->_('Date de diffusion de %s', $this->getMainTitleAsText()))];
 
     return $this->_view->badgeGroup($badges, $this);
   }
diff --git a/library/templates/Intonation/Library/View/Wrapper/Search.php b/library/templates/Intonation/Library/View/Wrapper/Search.php
index 5d3f8930045..ceec19ee46d 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Search.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Search.php
@@ -125,7 +125,7 @@ class Intonation_Library_View_Wrapper_Search
             ->setUrl($this->_view->url(array_merge($this->_model->getCriteriasUrl(),
                                                    ['controller' => 'bookmarked-searches',
                                                     'action' => $can_follow ? 'add' : 'delete',
-                                                    'label' => $this->getMainTitle(),
+                                                    'label' => $this->getMainTitleAsText(),
                                                     'id' => $id])))
             ->setImage($this->getIco($can_follow ? 'no-selection' : 'selection',
                                      'library'))
diff --git a/library/templates/Intonation/Library/View/Wrapper/Work.php b/library/templates/Intonation/Library/View/Wrapper/Work.php
index b34a3db0962..7cd94d2a664 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Work.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Work.php
@@ -48,7 +48,7 @@ class Intonation_Library_View_Wrapper_Work extends Intonation_Library_View_Wrapp
 
   public function getMainLink() {
     $title = $this->_('Voir l\'Å“uvre %s de %s',
-                      $this->getMainTitle(),
+                      $this->getMainTitleAsText(),
                       $this->getSecondaryTitle());
 
     $url = array_merge($this->_widget_context_params,
@@ -59,7 +59,7 @@ class Intonation_Library_View_Wrapper_Work extends Intonation_Library_View_Wrapp
 
     if ((string)$this->_model->getTypeDoc() === Class_TypeDoc::PAGE) {
       $title = $this->_('Voir la page %s',
-                        $this->getMainTitle());
+                        $this->getMainTitleAsText());
       $url = (new Class_Notice_Permalink)->relativeFor($this->_model);
     }
 
diff --git a/library/templates/Intonation/View/Jumbotron/Abstract.php b/library/templates/Intonation/View/Jumbotron/Abstract.php
index 2a87a6022f6..abfca51e233 100644
--- a/library/templates/Intonation/View/Jumbotron/Abstract.php
+++ b/library/templates/Intonation/View/Jumbotron/Abstract.php
@@ -93,7 +93,7 @@ abstract class Intonation_View_Jumbotron_Abstract extends ZendAfi_View_Helper_Ba
 
 
   protected function _getPageTitle($wrapped) {
-    return $wrapped->getMainTitle();
+    return $wrapped->getMainTitleAsText();
   }
 
 
diff --git a/library/templates/Intonation/View/RenderTimeline.php b/library/templates/Intonation/View/RenderTimeline.php
index 4676b24fd32..f08efaf0d3c 100644
--- a/library/templates/Intonation/View/RenderTimeline.php
+++ b/library/templates/Intonation/View/RenderTimeline.php
@@ -110,7 +110,7 @@ class Intonation_View_RenderTimeline extends Intonation_View_Abstract_Layout {
 
 
   protected function _getHeadline($element) : string {
-    return (string) $this->view->truncate($element->getMainTitle(),
+    return (string) $this->view->truncate($element->getMainTitleAsText(),
                                           [],
                                           3,
                                           false,
diff --git a/public/opac/css/core.css b/public/opac/css/core.css
index 0214e68d8e8..d8d51645386 100644
--- a/public/opac/css/core.css
+++ b/public/opac/css/core.css
@@ -225,3 +225,7 @@ body[data-admin_level=""] [data-level="expert"]  {
     top: 0;
     left: 0;
 }
+
+.whitespace_after:not(:last-child):after {
+    content: ' ';
+}
diff --git a/tests/library/Class/NoticeTest.php b/tests/library/Class/NoticeTest.php
index 238dcb3ceb7..5995965504d 100644
--- a/tests/library/Class/NoticeTest.php
+++ b/tests/library/Class/NoticeTest.php
@@ -1138,7 +1138,15 @@ class NoticeTitleWithSeveral461Test extends ModelTestCase {
               [461, ['t' => 'Cinéma']],
               [461, ['t' => 'Documentaire', 'v' => '9 novembre']]
              ]
-            ]
+            ],
+
+            [
+             'Le gros titre',
+             [
+              [200, ['a' => 'Le gros titre']],
+              [461, ['t' => 'Le gros titre']],
+             ]
+            ],
     ];
   }
 
diff --git a/tests/scenarios/IndexablePages/IndexablePagesSearchTest.php b/tests/scenarios/IndexablePages/IndexablePagesSearchTest.php
index af87fed9e59..5fff468b43c 100644
--- a/tests/scenarios/IndexablePages/IndexablePagesSearchTest.php
+++ b/tests/scenarios/IndexablePages/IndexablePagesSearchTest.php
@@ -93,6 +93,6 @@ class IndexablePagesSearchTest extends AbstractControllerTestCase {
                                     'resultat',
                                     'simple');
     $this->dispatch('/recherche/simple/expressionRecherche/event/by_work/1/id_profil/34');
-    $this->assertXPath('//div[contains(@class, "card_title_Intonation_Library_View_Wrapper_Work")]//a[text()="Met ton pass!"][@href="' . BASE_URL . '/dtc"][@title="Voir la page Met ton pass!"]');
+    $this->assertXPath('//div[@class= "card-title card_title card_title_Intonation_Library_View_Wrapper_Work"]/a[@href="' . BASE_URL . '/dtc"][@title="Voir la page Met ton pass!"]/span[text()="Met ton pass!"]');
   }
 }
diff --git a/tests/scenarios/Serials/SerialsDetailsTest.php b/tests/scenarios/Serials/SerialsDetailsTest.php
index 1a9236ae9ea..bdeb8a074b1 100644
--- a/tests/scenarios/Serials/SerialsDetailsTest.php
+++ b/tests/scenarios/Serials/SerialsDetailsTest.php
@@ -237,7 +237,8 @@ class SerialsDetailsRecordDescriptionPeriodiqueSerieTest extends SerialsDetailsT
 
   /** @test */
   public function pageShouldContainsMonArticle() {
-    $this->assertXPathContentContains('//a[@href="/recherche/viewnotice/id/1"]', 'Mon Article 1 : complement titre');
+    $this->assertXPathContentContains('//a[@href="/recherche/viewnotice/id/1"][@title="Voir le document Fakir Mon Article 1 : complement titre"]',
+                                      '<span class="461t 461t_1">Fakir</span><span class="text_whitespace"> </span><span class="200a">Mon Article 1</span><span class="text_whitespace"> </span><span class="200$e">: complement titre</span>');
   }
 
 
diff --git a/tests/scenarios/ShelfNavigation/ShelfNavigationTest.php b/tests/scenarios/ShelfNavigation/ShelfNavigationTest.php
index 6c5d33cd159..4c0ec2c7e3e 100644
--- a/tests/scenarios/ShelfNavigation/ShelfNavigationTest.php
+++ b/tests/scenarios/ShelfNavigation/ShelfNavigationTest.php
@@ -178,7 +178,7 @@ class ShelfNavigationSearchShelfInCenterTest
    * @dataProvider itemTitleAndPosition
    **/
   public function activePageInMiddleShouldContainsCardTitleForTitleAtPostion($title, $position) {
-    $this->assertXPath(sprintf('//div[contains(@class, "carousel-item")][3][contains(@class, "active")]//div[contains(@class, "card_with_overlay")][%s]//div[@class="card-title"][text()="%s"]',
+    $this->assertXPath(sprintf('//div[contains(@class, "carousel-item")][3][contains(@class, "active")]//div[contains(@class, "card_with_overlay")][%s]//div[@class="card-title"]/span[text()="%s"]',
                                $position,
                                $title));
   }
@@ -236,7 +236,7 @@ class ShelfNavigationSearchShelfAtEndTest extends ShelfNavigationSearchShelfTest
    * @dataProvider itemTitleAndPosition
    **/
   public function activePageInMiddleShouldContainsCardTitleForTitleAtPostion($title, $position) {
-    $this->assertXPath(sprintf('//div[contains(@class, "carousel-item")][3][contains(@class, "active")]//div[contains(@class, "card_with_overlay")][%s]//div[contains(@class, "card-title")][text()="%s"]',
+    $this->assertXPath(sprintf('//div[contains(@class, "carousel-item")][3][contains(@class, "active")]//div[contains(@class, "card_with_overlay")][%s]//div[contains(@class, "card-title")]/span[text()="%s"]',
                                $position,
                                $title));
   }
@@ -276,7 +276,7 @@ class ShelfNavigationSearchShelfAtStartTest
    * @dataProvider itemTitleAndPosition
    **/
   public function activePageAtStartShouldContainsCardTitleForTitleAtPostion($title, $position) {
-    $this->assertXPath(sprintf('//div[contains(@class, "carousel-item")][1][contains(@class, "active")]//div[contains(@class, "card_with_overlay")][%s]//div[contains(@class, "card-title")][text()="%s"]',
+    $this->assertXPath(sprintf('//div[contains(@class, "carousel-item")][1][contains(@class, "active")]//div[contains(@class, "card_with_overlay")][%s]//div[contains(@class, "card-title")]/span[text()="%s"]',
                                $position,
                                $title));
   }
diff --git a/tests/scenarios/Templates/TemplatesRecordsTest.php b/tests/scenarios/Templates/TemplatesRecordsTest.php
index f6df4ecbb9a..0cc35e521dc 100644
--- a/tests/scenarios/Templates/TemplatesRecordsTest.php
+++ b/tests/scenarios/Templates/TemplatesRecordsTest.php
@@ -297,7 +297,7 @@ class TemplatesRecordsViewnoticeDouble200Test extends AbstractControllerTestCase
 
   /** @test */
   public function cardTitleShouldContainsOnlyKanjis() {
-    $this->assertXPath('//div[contains(@class, "card-title")][text()="建築文化"]');
+    $this->assertXPath('//div[contains(@class, "card-title")]/span[text()="建築文化"]');
   }
 
 
@@ -1363,6 +1363,8 @@ class TemplatesRecordWithMultipleMoreDescriptionDataTest extends AbstractControl
     $unimarc = (new Class_NoticeUnimarc_Fluent)
       ->zoneWithContent('001', '12345')
 
+      ->zoneWithChildren('200', ['a' => 'Hong Time'])
+
       ->zoneWithChildren('461', ['t' => 'Lotus out of Water',
                                  'a' => 'Hong Ting',
                                  '0' => ''])
@@ -1390,6 +1392,18 @@ class TemplatesRecordWithMultipleMoreDescriptionDataTest extends AbstractControl
   }
 
 
+  /** @test */
+  public function pageTitleShouldNotContainsHtml() {
+    $this->assertXPathContentContains('//head/title', 'Lotus out of Water The Milking Song The Jester\'s Dance Hong Time / Description - ** nouveau profil **');
+  }
+
+
+  /** @test */
+  public function recordTitleShouldContainsSpanClass200A() {
+    $this->assertXPathContentContains('//main//h1', '<span class="461t 461t_1">Lotus out of Water</span><span class="text_whitespace"> </span><span class="461t 461t_2">The Milking Song</span><span class="text_whitespace"> </span><span class="461t 461t_3">The Jester\'s Dance</span><span class="text_whitespace"> </span><span class="200a">Hong Time</span>');
+  }
+
+
   /** @test */
   public function theMilkingSongShouldBeInDescriptionUnderContient() {
     $this->assertXPathContentContains('//dl/dt[text()="Contient"]/following-sibling::dd//ul//li//span[@class="more_description_data_title font-italic"]',
diff --git a/tests/scenarios/Templates/TemplatesSearchTest.php b/tests/scenarios/Templates/TemplatesSearchTest.php
index 01feef526f2..ad5322e0c58 100644
--- a/tests/scenarios/Templates/TemplatesSearchTest.php
+++ b/tests/scenarios/Templates/TemplatesSearchTest.php
@@ -1271,7 +1271,7 @@ class TemplatesSearchResultWithSearchTermHighLightedTest extends AbstractControl
 
   /** @test */
   public function herosShouldBeHighlightedInTitle() {
-    $this->assertXPathContentContains('//div[contains(@class, "card-title")]//a//span[@class="highlight font-weight-bold text_decoration_underline"]',
+    $this->assertXPathContentContains('//div[contains(@class, "card-title card_title card_title_Intonation_Library_View_Wrapper_Record_HighlightTerms")]/a/span[@class="200a"]/span[@class="highlight font-weight-bold text_decoration_underline"]',
                                       'héros');
   }
 
diff --git a/tests/scenarios/Templates/TemplatesTimelineWidgetTest.php b/tests/scenarios/Templates/TemplatesTimelineWidgetTest.php
index ccb2edf5e0b..f571d0e6690 100644
--- a/tests/scenarios/Templates/TemplatesTimelineWidgetTest.php
+++ b/tests/scenarios/Templates/TemplatesTimelineWidgetTest.php
@@ -81,14 +81,15 @@ class TemplatesTimelineWidgetInMainTest extends AbstractControllerTestCase {
   /** @test */
   public function ilNeigeAujourdHuiShouldBeLoadedInScript() {
     $this->assertXPathContentContains('//script',
-                                      '<div class=\"card-title\" role=\"heading\" aria-level=\"2\">Il neige aujourd');
+                                      '<div class=\"card-title\" role=\"heading\" aria-level=\"2\"><span class=\"200a\">Il neige aujourd',
+                                      $this->_response->getBody());
   }
 
 
   /** @test */
   public function therapieDeGroupeShouldBeLoadedInScript() {
     $this->assertXPathContentContains('//script',
-                                      '<div class=\"card-title\" role=\"heading\" aria-level=\"2\">Th\u00e9rapie de groupe');
+                                      '<div class=\"card-title\" role=\"heading\" aria-level=\"2\"><span class=\"200a\">Th\u00e9rapie de groupe');
   }
 }
 
diff --git a/tests/scenarios/Templates/TemplatesWidgetCarouselTest.php b/tests/scenarios/Templates/TemplatesWidgetCarouselTest.php
index 350c7026c25..f532a2ac617 100644
--- a/tests/scenarios/Templates/TemplatesWidgetCarouselTest.php
+++ b/tests/scenarios/Templates/TemplatesWidgetCarouselTest.php
@@ -358,13 +358,13 @@ class TemplatesWidgetCarouselRecordDomainTest extends TemplatesWidgetCarouselRec
 
   /** @test */
   public function carouselCardTitleShouldDisplayRecordLaJeuneFille() {
-    $this->assertXpath('//div[contains(@class, "card-title")][text()="La jeune fille"]');
+    $this->assertXpath('//div[contains(@class, "card-title")]/span[text()="La jeune fille"]');
   }
 
 
   /** @test */
   public function carouselCardTitleShouldDisplayRecordHarryPotter() {
-    $this->assertXpath('//div[contains(@class, "card-title")][text()="Harry Potter et le prisonnier d\'Azkaban"]');
+    $this->assertXpath('//div[contains(@class, "card-title")]/span[text()="Harry Potter et le prisonnier d\'Azkaban"]');
   }
 }
 
@@ -405,7 +405,7 @@ class TemplatesWidgetCarouselDelayedRecordWithDomainTest extends TemplatesWidget
   /** @test */
   public function carouselCardTitleShouldDisplayRecordLaJeuneFille() {
     $this->dispatch('/widget/render/render/ajax/only/body/widget_id/1/profile_id/1/render/ajax');
-    $this->assertXpath('//div[contains(@class, "card-title")][text()="La jeune fille"]');
+    $this->assertXpath('//div[contains(@class, "card-title")]/span[text()="La jeune fille"]');
   }
 
 
-- 
GitLab