Diazo

Author:Éric Bréhault (Makina Corpus)
Created:2013-04-17
Version:1.0

Introduction

Avant Diazo, la mise en place d’un design spécifique pour un site Plone se faisait en dérivant le thème de base de Plone afin d’en modifier certains éléments:

  • les images (par exemple le logo, ou le favicon),
  • les CSS,
  • les templates (template principal, template de portlets ou de viewlets).

Cela impliquait de changer de nombreux fichiers de déclaration, et souvent de ré-écrire certains fichiers Python.

Autrement dit, il fallait rentrer dans le coeur de Plone.

Diazo propose une approche extérieure: il se comporte comme un proxy web (autrement dit un service qui reçoit des URLs en entrée et renvoi le contenu HTML d’une page produite par un autre système en retour) mais il est capable d’appliquer dynamiquement un design sur le contenu qu’il renvoie.

Principe

Le design est fourni sous forme d’une arborescence HTML statique classique:

  • un ou plusieurs fichiers HTML,
  • des images,
  • des CSS,
  • des JS.

L’application de ce design aux contenus Plone est définie par un fichier de règles nommé rules.xml.

Ce fichier de règles permet de mettre en correspondance des éléments du contenu dynamique (par exemple le titre du contenu Plone) avec des éléments du thème, c’est-à-dire le modèle HTML statique, (par exemple tel tag H1 ayant l’identifiant “content-title”).

Éditeur en ligne de Diazo

Depuis la version 4.3, Plone propose un éditeur en ligne pour modifier le thème Diazo.

Se rendre dans la Configuration du site, puis dans Modules.

Installer Diazo theme support.

Aller ensuite dans Theming.

../_images/diazo-1.png

On peut créer un nouveau thème et il existe également un exemple minimal qui peut être copié.

Lorqu’on édite un thème, on dispose d’un navigateur qui permet de gérer les fichiers disponibles, et d’un éditeur texte.

../_images/diazo-1.png

On dispose également de 2 inspecteurs qui permettent de voir les pages du thème en HTML statiques et les pages Plone et d’y sélectionner à la souris les éléments qu’on veut mettre en correspondance:

  • un clic permet de pointer un élément,
  • la touche Esc permet de pointer son père,
  • la touche Enter place l’élement dans la sélection courante,
  • le bouton Build rule permet de créer une règle à partir des sélections.

Les directives

Le fichier rules.xml peut contenir les directives suivantes.

<theme />

Cette directive applique une page de thème. Exemple:

<theme href="index.html" />

On peut rendre y ajouter des conditions afin de choisir quand utiliser telle ou telle page de thème. Exemple:

<theme href="index.html" css:if-content=".section-front-page" />
<theme href="detail.html" css:if-not-content=".section-front-page" />

Les attributs possibles pour <theme/> sont:

  • css:if-content et css:if-not-content qui permettent de tester un sélecteur CSS sur le contenu Plone,

  • if-content et if-not-content qui permettent de tester un sélecteur XPath sur le contenu Plone,

  • if qui permet de tester une variable Plone (voir plus loin), par exemple:

    <theme href="theme-two-left.html" if="$have_left_portlets"/>
  • if-path qui permet de tester des conditions sur la requête courante, exemple:

    <theme href="calendrier.html" if-path="/events"/>

Note

pour plus de détails sur les conditions, voir plus bas.

<notheme />

Cette directive désactive le thème diazo. Exemple:

<notheme css:if-content=".template-overview-controlpanel" />

Les attributs possibles pour <notheme/> sont:

  • css:if-content et css:if-not-content,
  • if-content et if-not-content,
  • if,
  • if-path.

<replace />

Cette directive remplace un élément du thème par le contenu Plone. Exemple:

<replace theme="/html/head/title" content="/html/head/title"/>

Les attributs possibles pour <replace /> sont:

  • theme ou css:theme pour désigner l’élément cible dans le thème HTML,

  • content ou css:content pour désigner l’élément source dans le contenu Plone,

  • theme-children ou css:theme-children, qui permet de remplacer non pas l’élément désigné, mais tous les tags qu’il contient,

  • content-children ou css:content-children, qui permet d’utiliser non pas l’élément désigné, mais tous les tags qu’il contient,

  • attributes qui permet de remplacer des attributs plutôt que le contenu. Exemple:

    <replace theme="/html/body" content="/html/body" attributes="class"/>
  • css:if-content et css:if-not-content,

  • if-content et if-not-content,

  • if,

  • if-path,

  • method (voir plus bas).

<before /> et <after />

Ces deux directives ont un comportement similaire à <replace /> mais plutôt que remplacer l’élément ciblé, elles vont faire une insertion avant ou après la cible (selon le cas).

Les attributs possibles sont les mêmes que pour <replace /> sauf method.

<append /> et <prepend />

Ces deux directives ont un comportement similaire à <replace /> mais plutôt que remplacer l’élément ciblé, elles vont faire une insertion à l’intérieur de l’élement cible à la fin ou au début (selon le cas).

Les attributs possibles sont les mêmes que pour <replace /> sauf method.

<drop />

Cette directive supprime des éléments du thème ou du contenu. Exemples:

<drop css:content="#portal-content .documentByLine" />
<drop css:theme=".lorem-ipsum" />

Note

une même commande <drop /> ne peut pas supprimer à la fois dans le contenu et dans le thème.

Les attributs possibles pour <drop /> sont:

  • theme ou css:theme,
  • content ou css:content,
  • theme-children ou css:theme-children,
  • content-children ou css:content-children,
  • attributes,
  • css:if-content et css:if-not-content,
  • if-content et if-not-content,
  • if,
  • if-path.

<strip />

Cette directive supprime des élements sans supprimer leur contenu. Exemple:

<strip css:content=".portletWrapper" />

va supprimer les tags div de classe portletWrapper mais les tags dl des portlets seront préservés.

Les attributs possibles sont les mêmes que pour <drop />.

<merge />

Cette directive fusionne pour un attribut donné les valeurs provenant du thème et celles provenant du contenu Plone. Exemple:

<merge attributes="class" css:theme="body" css:content="body" />

Les attributs possibles sont:

  • attributes, qui définit les attributs concernés (séparés par des espaces),
  • separator, qui indique le séparateur à utiliser pour fusionner les valeurs (par défaut, c’est un espace),
  • theme ou css:theme,
  • content ou css:content,
  • css:if-content et css:if-not-content,
  • if-content et if-not-content,
  • if,
  • if-path.

<copy />

Cette directive permet de copier un attribut venant du contenu Plone sur un élément du thème.

Note

À la différence, de <replace attributes />, <copy /> va fonctionner même si l’attribut n’existe pas sur l’élément cible (et s’il existe, il est remplacé).

Exemple:

<copy attributes="class" css:theme="body" css:content="body"/>

Les attributs possibles sont:

  • attributes, qui définit les attributs concernés (séparés par des espaces),
  • theme ou css:theme,
  • content ou css:content,
  • css:if-content et css:if-not-content,
  • if-content et if-not-content,
  • if,
  • if-path.

L’ordre d’éxecution

Dans certains cas, l’ordre d’éxecution peut avoir des conséquence sur résultat, il est donc bon de le connaître.

L’ordre est le suivant:

  1. les directives <notheme />,
  2. les directives <theme />,
  3. les directives <before /> utilisant theme (et non pas theme-children),
  4. les directives <drop /> sont éxécutées ensuite,
  5. les directives <replace /> utilisant theme (et non pas theme-children),
  6. les directives <strip />,
  7. toutes les directives s’appliquant aux attributs,
  8. les directives <before />, <replace /> et <after /> utilisant theme-children,
  9. les directives <after /> utilisant theme (et non pas theme-children).

Note

si on ajoute method="raw" sur une directive <replace />, on peut utiliser un morceau du contenu Plone même s’il appartient à un élément supprimé par un <drop />.

Cas des règles sans correspondance

Si une règle utilise un theme (ou css:theme) ne correspondant à aucun élément dans le thème, la règle est ignorée.

En revanche, si une règle utilise un content (ou css:content) ne correspondant à aucun élément dans le contenu, la règle sera appliqué (et le contenu sera simplement considéré comme vide).

Les conditions

Conditions sur le contenu

if-content permet d’indiquer une expression XPath, si cette expression trouve une correspondance dans le contenu, la règle sera appliquée.

css:if-content fonctionne de la même manière, mais en utilisant une expression CSS plutôt qu’XPath.

Exemple:

<drop css:theme="#publicite" css:if-content=".userrole-authenticated"/>

signifie que l’élément publicite sera supprimé si la classe userrole-authenticated est présente (autrement dit si l’utilisateur est connecté).

Si on laisse if-content vide, cela revient à utiliser la même valeur que celle de content (ou css:content). Exemple:

<after css:theme="#topnews dt" css:content=".portlet-topnews dd"
    css:if-content=""/>

est équivalent à:

<after css:theme="#topnews dt" css:content=".portlet-topnews dd"
    css:if-content=".portlet-topnews dd"/>

Si plusieurs règles concernent le même élement du thème mais ont des conditions différentes, cela va être interprêté comme un bloc if ... then ... else .... Exemple:

<replace theme-children="/html/body/h1" content="/html/body/h1/text()" if-content="/html/body/h1"/>
<replace theme-children="/html/body/h1" content="//h1[@id='first-heading']/text()" if-content="//h1[@id='first-heading']"/>
<replace theme-children="/html/body/h1" content="/html/head/title/text()" />

va renseigner le tag H1 du body de la façon suivante:

  • si le contenu dispose d’un tag h1 comme fils immédiat du body, on en prend le texte,
  • sinon, on prend n’importe quel h1 dont l’id est ‘first-heading’,
  • et s’il n’y en a pas non plus, on prend la valeur du title.

Conditions sur le chemin de la page

L’attribut if-path permet de conditionner l’application d’une règle en fonction du chemin de la page la page demandée.

Si la valeur commence par /, on cherche la correspondance à partir du début du chemin. Par exemple:

<drop css:theme="#search" if-path="/blog" />

va s’appliquer pour /blog, /blog/, et /blog/recent.

Si la valeur se termine par /, on cherche la correspondance à partir de la fin du chemin. Par exemple:

<drop css:theme="#search" if-path="recent/" />

va s’appliquer pour /blog/recent/, et /actualites/interne/recent.

Pour une correspondance exacte, il faut mettre / au début et à la fin. Par exemple:

<drop css:theme="#search" if-path="/blog/" />

va s’appliquer pour /blog, et /blog/, mais pas /blog/recent.

S’il n’y a de / ni à la fin ni au début, la correspondance se fait sur n’importe quelle partie du chemin. Par exemple:

<drop css:theme="#search" if-path="interne/recent" />

va s’appliquer pour /interne/recent, /actualites/interne/recent, et /actualites/interne/recent/images.

On peut indiquer plusieurs conditions en les séparant par des espaces:

<drop css:theme="#search" if-path="/blog /agenda" />

Conditions sur des variables

Le fichier manifest.cfg permet de définir des variables dans la section [theme:parameters]. Exemple:

[theme:parameters]
have_left_portlets = python:context.restrictedTraverse('@@plone').have_portlets('plone.leftcolumn',context)
have_right_portlets = python:context.restrictedTraverse('@@plone').have_portlets('plone.rightcolumn',context)
have_both_portlets = python:context.restrictedTraverse('@@plone').have_portlets('plone.leftcolumn',context) and context.restrictedTraverse('@@plone').have_portlets('plone.rightcolumn',context)
lang = python:context.restrictedTraverse("@@plone_portal_state/language")()

Note

les modifications du fichier manifest.cfg ne sont pas prises en compte lorsqu’on utilise l’éditeur en ligne. Il faut les saisir dans l’onglet Paramètres avancés.

L’attribut if (ou if-not) permet d’utiliser ces variables en les préfixant par un $:

<theme href="theme-two-left.html" if="$have_left_portlets"/>
<theme href="index-fr.html" if="$lang = 'fr'"/>

Groupement et encapsulation de conditions

On peut regrouper des règles dans des balises <rules /> afin de leur appliquer des conditions communes:

<rules
    xmlns="http://namespaces.plone.org/diazo"
    xmlns:css="http://namespaces.plone.org/diazo/css"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <rules css:if-content=".portlet-alerte">
        <after css:theme-children="#header" css:content=".portlet-alerte #message"/>
        <before css:theme-children="#main" css:content=".portlet-alerte #warning-icon"/>
    </rules>

    ...

</rules>

Et on peut également mettre ces balises <rules /> en cascade:

<rules css:if-content=".section-Members">
    <rules css:if-content=".portlet-alerte">
        <after css:theme-children="#header" css:content=".portlet-alerte #message"/>
    </rules>
    <rules css:if-content=".portlet-todo">
        <after css:theme-children="#header" css:content=".portlet-todo #message"/>
    </rules>
</rules>

Conditions sur <theme />

On peut fournir plusieurs directives <theme />:

<theme href="news.html" css:if-content=".section-news"/>
<theme href="members.html" css:if-content=".section-Members"/>
<theme href="index.html"/>

Les différentes conditions sont testées dans l’ordre où elles sont fournies, et c’est la première qui donne une correspondance qui est retenue.

En revanche les directives <notheme /> ont toujours la priorité sur les directives <theme /> quel que soit l’ordre.

Toutes les règles sont appliquées quelle que soit la directive <theme /> retenue sauf si on utilise l’encapsulation:

<rules css:if-content=".section-news">
    <theme href="news.html"/>
    <copy css:content="h2.articleheading" css:theme="h1"/>
</rules>

Modification du thème à la volée

On peut utiliser une règle pour injecter non pas du contenu mais du HTML statique:

<after theme-children="/html/head" if="$isUserBirthday">
    <style type="text/css">
        * { font-family: Comic Sans MS; }
    </style>
</after>

On peut aussi modifier les éléments du thème. Par exemple:

<replace css:theme="#details">
    <dl id="details">
        <xsl:for-each css:select="table#details > tr">
            <dt><xsl:copy-of select="td[1]/text()"/></dt>
            <dd><xsl:copy-of select="td[2]/node()"/></dd>
        </xsl:for-each>
    </dl>
</replace>

va convertir un tableau en une liste de dt/dd.

Note

dans cet exemple on utilise css:select directement dans l’expression XSL car le pré-compilateur Diazo va faire la conversion en XPath.

Modification du contenu à la volée

On peut modifier le contenu en utilisant la directive <replace />:

<replace css:content="div#portal-searchbox input.searchButton">
    <button type="submit">
        <img src="images/search.png" alt="Search" />
    </button>
</replace>

Modification du résultat par <xsl />

Une fois toutes les règles appliquées, le rendu final peut encore être modifié par des directives <xsl />. Par exemple:

<xsl:template match="input/@type[.='submit']">
    <xsl:attribute name="class">btn</xsl:attribute>
</xsl:template>

va mettre class="btn" sur les inputs de type submit (btn étant la classe CSS Bootstrap pour les boutons, c’est un moyen intéressant de satisfaire le markup d’un framework comme Bootstrap sans devoir modifier de l’intérieur le markup produit par Plone).

Pour ajouter une classe:

<xsl:template match="div/@class[contains(., 'summary')]">
    <xsl:attribute name="class">draggable <xsl:value-of select="."/></xsl:attribute>
</xsl:template>

va ajouter la classe draggable à tous les div portant la classe summary.

Autre exemple, pour transformer les portlets Plone dont le markup est:

<dl>
    <dt>titre</dt>
    <dd>un item</dd>
    <dd>un autre item</dd>
</dl>

en un markup plus simple tel que:

<div>
    <h6>titre</h6>
    <div>
        un item
        un autre item
    </div>
</div>

(notamment parce qu’il est utile d’avoir un wrapper du contenu du portlet en entier, pour le rendre collapsible par exemple).

Cette transformation sera assurée par le template xsl suivant:

<xsl:template match="dl[@class[contains(., 'portlet')]]">
    <div class="portlet">
        <h6 class="portlet-header">
            <xsl:copy-of select="./dt/*" />
        </h6>
        <xsl:apply-templates select="./dd" />
    </div>
</xsl:template>

Note

les directives <xsl /> doivent être placées à la racine de la balises <rules /> principale, et elles sont appliquées de façon inconditionnelle.

Factorisation

On peut ré-utiliser des règles dans différents thèmes Diazo en utilisant la balise <xi:include />. Exemple:

<xi:include href="standard-rules.xml" />

Insertion de contenu externe

Normalement le contenu est fourni par la page Plone correspondant à l’URL demandée.

Mais en utilisant l’attribut href, on peut demander à une règle d’utiliser une autre page:

<append css:theme="#right-column" css:content="#content" href="/Plone/Members" />

On peut même aller chercher une page servie par un autre serveur si on active le mode réseau dans les Paramètres avancés de Diazo:

<append css:theme="#right-column" css:content="#current" href="http://www.plone.org/" />

Note

cela rend le site courant dépendant d’un service extérieur, c’est donc relativement risqué, car si ce serveur externe n’est pas disponible, le thème est cassé.

On peut utiliser des méthodes d’insertion plus robustes si on dispose d’un frontal web proposant SSI ou ESI:

<after css:theme-children="#left-column" css:content="#portlet"
    href="/extra.html" method="ssi"/>

<after css:theme-content="#left-column" css:content="#portlet"
    href="/extra.html" method="esi"/>

Insertion d’un fichier HTML du thème

Avec cette même approche, on peut inclure un fichier HTML statique du thème en utilisant son chemin propre:

<append css:theme="#right-column"
        css:content="body"
        href="/Plone/++theme++montheme/footer.html" />

Bonnes pratiques et techniques standards

Utiliser un framework CSS

Diazo facilite énormément l’adoption d’un framework CSS puisqu’il suffit de déposer les fichiers du framework directement dans le dossier des ressources statiques du thème.

L’intérêt d’utiliser un framework CSS (comme 960, Bootstrap, Foundation) a déjà été largement démontré, et ne fait pas l’objet de ce document.

Plone en tant que back-office

Avec Diazo, les fonctionnalités de gestion de contenu de Plone sont utilisées comme un back-office qui alimente la couche de rendu.

Ainsi on va par exemple ajouter des portlets non pas pour les afficher en tant que portlets mais comme fournisseurs de contenu à tel élément du thème Diazo.

Un exemple typique est l’utilisation de collective.portlet.sitemap pour fournir les entrées d’un menu déroulant.

De même, on peut utiliser collective.masonry pour créer des conteneurs de portlets qui permettront l’alimenter tel ou tel bloc du modèle HTML.

Supprimer les commentaires et le Lorem Ipsum

Il peut être utilise de mettre dans les modèles HTML du thème des commentaires ou du texte de remplissage afin que son affichage en statique soit plus représentatif qu’un markup complètement vierge.

Mais bien entendu on ne souhaite pas les voir apparaître lors du rendu des contenus.

Une technique simple consiste à mettre une classe (par exemple drop) sur ces éléments:

<div id="main-content" class="cell width-1">
    <p class="drop">
        Suave, mari magno turbantibus aequora ventis,
        e terra magnum alterius spectare laborem;
        non quia vexari quemquamst jucunda voluptas,
        sed quibus ipse malis careas quia cernere suavest.
    </p>
</div>

Et on les élimine grâce à une règle de ce type:

<drop css:theme=".drop" />

Cela permet également de supprimer temporairement certaines partie du design.

Supprimer les paragraphes vides

L’éditeur TinyMCE laisse parfois des balises <p/> vides. Cette règle permet de les éliminer:

<drop content="p[not(*) and (not(normalize-space()) or text() = '&#160;')]"/>

Les éléments du <head/>

Le tag <head/> produit par Plone contient de nombreux éléments importants voire obligatorei pour un bon comportement du site. En général on reprend les éléments suivants:

<replace css:theme="title" css:content="title" />
<before css:theme-children="head" css:content="base" />
<before theme-children="/html/head" content="/html/head/meta" />
<before theme-children="/html/head" content="/html/head/link | /html/head/style | /html/head/comment()" />
<before theme-children="/html/head" content="/html/head/script" />

Note

on ne peut malheureusement pas mettre les scripts à la fin du body car cela casse un bon nombres de scripts inline.

Accessoirement, on peut souhaiter éliminer le viewport (et s’appuyer sur le comportement du framework CSS retenu):

<drop content='/html/head/meta[@name="viewport"]'/>

On peut aussi alimenter les meta de réseaux sociaux comme Facebook ou Twitter (à moins d’utiliser un module Plone qui s’en charge bien entendu):

<after theme-children="/html/head/meta">
    <meta property="og:image" content="http://www.monsite.fr/++theme++montheme/images/logo.png" />
    <meta name="twitter:card" content="summary">
    <meta name="twitter:site" content="@moncomptetwitter">
</after>

Le snippet javascript pour les statistiques

Le snippet javascript renseigné dans la configuration du site Plone (pour les statistiques GoogleAnalytics, ou Piwic, ou autre) n’est pas injecté dans le <head/> de Plone donc les règles précédentes ne le traite pas.

Il faut ajouter:

<after theme-children="/html/body" content="/html/body/div[@id='visual-portal-wrapper']/div/script" />

Créer un tag englobant

Dans cet exemple, on place le titre et la description dans un <div /> englobant:

<replace css:content-children="#content" css:theme-children="#content"/>
<before css:theme-children="#content">
    <div id="wrapper">
        <xsl:apply-templates css:select=".documentFirstHeading" mode="raw"/>
        <xsl:apply-templates css:select=".documentDesciption" mode="raw"/>
    </div>
</before>
<drop css:content=".documentFirstHeading"/>
<drop css:content=".documentDesciption"/>

Explications:

  • le <before /> insère le tag <div /> et 2 appels xsl permettent d’y ajouter le titre et la description,
  • les <drop /> évitent que le titre et la description soient répétés (puisqu’ils figurent dans #content),
  • le mode="raw" est nécessaire car les <drop /> sont appliqués avant le <before css:theme-children /> (en XSLT, le mode raw permet qu’un noeud soit traité plusieurs fois).

Ajouter ou modifier des attributs

Pour un ajout:

<xsl:template match="a">
    <xsl:copy>
        <xsl:attribute name="target">_blank</xsl:attribute>
        <xsl:copy-of select="@*" />
        <xsl:apply-templates />
    </xsl:copy>
</xsl:template>

Explications:

  • on copie l’élement,
  • on y ajoute l’attribut (ici target),
  • on recopie ensuite les attributs existants (y compris target s’il était déjà défini),
  • puis on insère les noeuds fils

Pour une modification:

<xsl:template match="img/@src[not(contains(., '@@'))]">
    <xsl:attribute name="src"><xsl:value-of select="." />/@@/images/image/thumb</xsl:attribute>
</xsl:template>

Autre exemple, supprimer une classe CSS (sans supprimer les autres):

<xsl:template match="form/@class[contains(., 'enableFormTabbing')]">
    <xsl:attribute name="class"><xsl:value-of select="concat(substring-before(., 'enableFormTabbing'), substring-after(., 'enableFormTabbing'))"/></xsl:attribute>
</xsl:template>

Debugging

Lorsqu’on fait fonctionner Zope en mode debug (à priori quand on l’a lancé avec bin/ploncectl fg), on peut ajouter des commandes diazo dans l’url:

  • ?diazo.debug=1 (par exemple http://localhost:8080/Plone?diazo.debug=1) va afficher à l’écran les traces d’éxécution de Diazo (les erreurs, les règles appliquées (en vert), les règles écratées (en rouge))
../_images/debug.png
  • ?diazo.off=1 (par exemple http://localhost:8080/Plone?diazo.off=1) va désactiver Diazo, de manière par exemple à pouvoir inspecter la page non thémée plus facilement.

Accès spécial non-thémé

La configuration avancée de Diazo (Configuration du site / Theming / Advanced Settings) permet de déclarer des noms de domaine pour lesquels Diazo ne sera pas activer.

Par défaut, il y a 127.0.0.1.

Ainsi on peut, si on le souhaite, fournir aux conributeurs un accès “back-office” sans theming.


blog comments powered by Disqus