11528 sujets

JavaScript, DOM et API Web HTML5

Bonjour,

Mon environnement est Node.js et Fastify, du JavaScript donc.

Le contenu de mes articles (`data`) est parsé par plusieurs regex ayant pour but de transformer des shortcodes "maison" par des balises HTML appropriées (lecteur YouTube, map Leaflet, etc). Ici il s'agit de rendre une image.

Mon shortcode est défini ainsi :
{{"/medias/images/uploads/clay-banks-original.webp" "Yaquina Head, Newport, États-Unis." align="right" width="2002" height="3000"}}

Le rendu HTML généré est celui-ci (le code généré n'a pas d'indentation, je le fais ici pour la lisibilité) :
<figure class="figure-image-focus-thumbnail-alignright">
  <picture>
    <source srcset="/medias/images/uploads/clay-banks-w600.webp, /medias/images/uploads/clay-banks-w1000.webp 2x">
    <img src="/medias/images/uploads/clay-banks-original.webp" alt="Yaquina Head, Newport, États-Unis." width="2002" height="3000">
  </picture>
  <figcaption>Yaquina Head, Newport, États-Unis.</figcaption>
</figure>

Nota bene : je sais que faire un alt et un figcaption identique "c'est pas bien", mais pour l'instant je développe, je ferais une itération du code plus tard...

Pour vous faire une idée voici un rendu visuel (il s'agit ici du framework statique uniquement, mais ça donne la même chose avec ma solution backend) : Article . Scriptura

Et maintenant voici le script qui me permet d'obtenir ce résultat (je ne mets ici que le shortcode pour les images) :
export default async function shortcodes(data) {
  /* Sont inclus ici les shortcodes qui précèdent */

  /**
   * Images
   * 1. Le premier paramètre, obligatoire, doit être une image valide.
   * 2. Le deuxième paramètre, obligatoire, est une description de l'image.
   * 3. Les autres paramètres sont facultatifs et doivent être nommés, ils sont insérés dans le tag HTML img.
   * 4. Le paramètre align="right", s'il est présent, doit être inséré dans la balise figure.
   * @param {string} data Un shortcode décrivant l'image : l'URL et une description chancun entre guillemets doubles, ensuite des paramètres nommés, facultatifs.
   * @returns {string} data Les tags HTML de l'image.
   * @see  https://regex101.com/r/vxAbSB/1
 
   * Exemple de paramètre en entrée : {{"/medias/images/uploads/clay-banks-original.webp" "Yaquina Head, Newport, États-Unis." align="right" width="2002" height="3000"}}
   */
  data = data.replace(
    /\{\{\s*"([^"]+\.(webp|avif|jpg|jpeg|png|gif))"\s*"([^"]+)"(?:\s*(align="right"))?(?:\s*width="(\d+)"\s*height="(\d+)")?\s*\}\}/gi,
    (match, src, ext, alt, align, width, height) => {
      const classAlign = align ? `-alignright` : `-alignleft`
      const imgSrc = `src="${src}"`
      const imgSrcset = `<source srcset="${src.replace(/-original\./, `-w600.`)}, ${src.replace(/-original\./, `-w1000.`)} 2x">`
      const imgAlt = `alt="${alt}"`
      const imgTag = `<img ${imgSrc} ${imgAlt} width="${width}" height="${height}">`
      const figcaption = `<figcaption>${alt}</figcaption>`

      return `<figure class="figure-image-focus-thumbnail${classAlign}"><picture>${imgSrcset}${imgTag}</picture>${figcaption}</figure>`
    },
  )
  return data
}

Et maintenant ma problématique : mes attributs doivent respecter un ordre précis pour être compris par ma regex, or j'aimerais que les définitions facultatives puissent être annoncées comme bon me semble (à la limite ont pourrait laisser de côté les deux premières définitions obligatoires qui resteraient en premier). Par exemple j'aimerais pouvoir détecter ceci :
{{"/medias/images/uploads/clay-banks-original.webp" "Yaquina Head, Newport, États-Unis." height="3000" width="2002" align="right"}}

Comment faire cela ? Peut être une solution à partir de "drapeaux" ? Mais alors moi et les regex... je m'en sors sur un point ou un autre et puis j'oublie très vite...
Modifié par Olivier C (20 Feb 2024 - 06:54)
Modérateur
Bonjour,

Si tu n'as que quelques attributs à capturer, tu peux utiliser simplement des "|" pour fabriquer un motif de base capturant l'un des attributs et répéter ce motif de base autant de fois que le nombre maximum d'attributs concernés :
s='{{"/medias/images/uploads/clay-banks-original.webp" "Yaquina Head, Newport, États-Unis." align="right" height="300" width="200"}}';
s=s.replace(/\{\{"([^"]*)"\s*"([^"]*)"\s*((height="\d+")|(width="\d+")|(align="right"))?\s*((height="\d+")|(width="\d+")|(align="right"))?\s*((height="\d+")|(width="\d+")|(align="right"))?\}\}/,"<img src=\"$1\" alt=\"$2\" $3 $7 $11>");
console.log(s);


Amicalement,
C'est pas mal. Cependant les captures ne sont plus récupérées dans l'ordre de la fonction :
(match, src, ext, alt, align, width, height) => {}

Ce qui me pose problème ensuite pour dispatcher les différents attributs ou classes CSS là où ils doivent être.
Modérateur
Olivier C a écrit :
C'est pas mal. Cependant les captures ne sont plus récupérées dans l'ordre de la fonction :


Salut,

Dans ce cas, utilise les assertions nommées que tu pourras récupérer avec match.groups Smiley cligne

exemple :
pattern
(?<id>\d+)-(?<slug>[a-z0-9_-]+)\.html

string
/1234-un-slug.html


ce qui donne :

let str = '/1234-un-slug.html'
let pattern = /(?<id>\d+)-(?<slug>[a-z0-9_-]+)\.html/

console.log(str.match(pattern).groups)



{
  "id": "1234",
  "slug": "un-slug"
}


match
Modifié par niuxe (19 Feb 2024 - 12:25)
@niuxe : ah oui les "Groupes de capture nommés", je crois que c'est ça que j'appelais plus haut des "drapeaux".

Bon ben faut y aller. J'espère que je vais m'en sortir parce que ce n'est pas gagné. J'essaierais en soirée. Je vous fais un retour si j'y arrive.

Édit : je viens de mettre une page test ici avec tous les cas de figure à récupérer : Regex101.
Modifié par Olivier C (19 Feb 2024 - 14:45)
Salut,

je viens pas aider Smiley rolleyes , je ne connaissais pas avant de voir ce message, mais je ne pige pas bien l’intérêt d'avoir des "shortcodes". Cela ne serait pas plus simple d'avoir directement un format json Smiley hein ?

Ton exemple donnerait un truc du genre :
{{src: "/medias/images/uploads/clay-banks-original.webp", alt : "Yaquina Head, Newport, États-Unis.", align:"right", width:"2002", height:"3000"}}


Et ainsi pouvoir directement faire
data.src 
Meilleure solution
@Mathieuu : mais dis-donc, c'est super intéressant cette approche ! Super propre et tout et tout ! Le code s'en retrouverait grandement simplifié, un très bon point pour des itérations futures et pour la maintenance du code.

Le parsage d'un JSON est moins performant qu'une regex javascript, environ 4 fois moins performant pour ce type de code, et puis il faudra toujours une regex pour la délimitation, donc deux passages. Mais dans un contexte de rendu de page ça doit être insignifiant, surtout avec une bonne mise en cache.

Ça vaux vraiment le coup de tenter l'opération. Aller je teste.
Bonsoir,

Si tu veux rester dans ton idée de base, le plus simple serait sans doute de le faire en deux fois:


html = code.replace(/\{\{"([^"]*)" "([^"]*)"((?:\s+\w+="[^"]*")*)\}\}/g, match=>{
let src = match[1];
let alt = match[2];
let attrs = {};
match[3].replace(/(\w+)="([^"]*)"/g, m=>attrs[m[1]]=m[2]);
return `<figure ...... > 
<img src="${src}" alt="${alt}" .... />
<figcaption>${alt}</figcaption>
......
</figure>`;
});


Mais l'idée du JSON est intéressante. Peut-être pas du point de vue de la longueur du short code, mais pour la simplicité du code js, vu qu'il suffirait alors de:


html = replace(/\{(\{[^{}]*\})\}/g, match=>{
let attrs = JSON.parse(match[1]);
return ` ..... `;
});
Bon et bien j'ai tenté les trois propositions :
1) Celle de parsimonhi, la plus simple à mettre en oeuvre (pour moi) mais qui reste peu souple (ce qui ne m’empêche pas de te remercier au passage).

2) Celle de Niuxe (merci à toi aussi) ; là je dois dire que j'ai échoué ; replace() c'est ok, mais alors passer par match()... je ne sais comment intégrer les résultats obtenus, je me suis perdu dans la stratosphère de la complexité (et je n'ai pas réussit à prendre les paramètres dans le désordre de toute façon) :
const pattern = /\{\{\s*"(?<src>[^"]*)"\s*"(?<description>[^"]*)"\s*align="(?<align>[^"]*)"\s*width="(?<width>[^"]*)"\s*height="(?<height>[^"]*)"\s*\}\}/g
let matches
const result = []

while ((matches = pattern.exec(string)) !== null) {
    result.push({
        src: matches.groups.src,
        description: matches.groups.description,
        align: matches.groups.align,
        width: matches.groups.width,
        height: matches.groups.height
    })
}

console.log(result); // ça donne quelque chose de pas mal en tableau, mais je ne sais pas comment l'exploiter... Avec un .map() sans doute ? mais il me reste la problématique de l'ordre des paramètres.


3) Enfin, la proposition inattendue de Mathieuu. Là encore j'ai un peu galéré à comprendre comment procéder mais voici une solution qui non seulement prend les paramètres dans n'importe quel ordre, mais certains peuvent être optionnels sans problème (il y a cependant des cas importants à gérer comme les "alt", cf. code ci après) et en plus on pourra rajouter des paramètres à l'avenir sans que cela fasse tout péter. Je prends :
let data = `<div>
{{"img": {"src": "https://medias/images/uploads/clay-banks-original.webp", "alt": "Image représentant un phare en bord de mer", "description": "Yaquina Head, Newport, États-Unis.", "align": "right", "width": "2002", "height": "3000"}}}
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
{{"img": {"src": "/medias/images/uploads/clay-banks-original.webp", "description": "Yaquina Head, Newport, États-Unis.", "height": "3000", "align": "right", "width": "2002"}}}
{{"img": {"src": "/medias/images/uploads/clay-banks-original.webp", "description": "Yaquina Head, Newport, États-Unis.", "height": "3000", "width": "2002"}}}
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.</p>
{{"img": {"src": "https://medias/images/uploads/clay-banks-original.webp", "align": "right", "width": "2002", "height": "3000"}}}
</div>`;

data = data.replace(/{{"img":\s*{(.*?)}}}/g, (match, imgData) => {
  const { src, alt, description, align, width = 'auto', height = 'auto' } = JSON.parse('{' + imgData + '}')
  const imgSrcset = `<source srcset="${src.replace(/-original\./, `-w600.`)}, ${src.replace(/-original\./, `-w1000.`)} 2x">`
  const classAlign = align ? `-alignright` : `-alignleft`
  const figcaption = description ? `<figcaption>${description}</figcaption>` : ''
  return `<figure class="figure-image-focus-thumbnail${classAlign}"><picture>${imgSrcset}<img src="${src}" alt="${
    alt || description || `Une image`
  }" width="${width}" height="${height}"></picture>${figcaption}</figure>`
})

console.log(data)

Ce qui retournera :
"<div>
<figure class='figure-image-focus-thumbnail-alignright'><picture><source srcset='https://medias/images/uploads/clay-banks-w600.webp,  https://medias/images/uploads/clay-banks-w1000.webp  2x'><img src='https://medias/images/uploads/clay-banks-original.webp' alt='Image représentant un phare en bord de mer' width='2002' height='3000'></picture><figcaption>Yaquina Head, Newport, États-Unis.</figcaption></figure>
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<figure class='figure-image-focus-thumbnail-alignright'><picture><source srcset='/medias/images/uploads/clay-banks-w600.webp, /medias/images/uploads/clay-banks-w1000.webp 2x'><img src='/medias/images/uploads/clay-banks-original.webp' alt='Yaquina Head, Newport, États-Unis.' width='2002' height='3000'></picture><figcaption>Yaquina Head, Newport, États-Unis.</figcaption></figure>
<figure class='figure-image-focus-thumbnail-alignleft'><picture><source srcset='/medias/images/uploads/clay-banks-w600.webp, /medias/images/uploads/clay-banks-w1000.webp 2x'><img src='/medias/images/uploads/clay-banks-original.webp' alt='Yaquina Head, Newport, États-Unis.' width='2002' height='3000'></picture><figcaption>Yaquina Head, Newport, États-Unis.</figcaption></figure>
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.</p>
<figure class='figure-image-focus-thumbnail-alignright'><picture><source srcset='https://medias/images/uploads/clay-banks-w600.webp,  https://medias/images/uploads/clay-banks-w1000.webp  2x'><img src='https://medias/images/uploads/clay-banks-original.webp' alt='Une image' width='2002' height='3000'></picture></figure>
</div>"

Merci à Mathieuu, merci à vous tous.

Sujet résolu.
Modifié par Olivier C (20 Feb 2024 - 06:43)
Salut,

ok je comprends un peu mieux, en fait dans data il n'y a pas que des "shortcode" il y a aussi du code html.

C'est rempli comment / par qui ce champs data ?
En l'état ça fait un genre de format à mi chemin entre du json et du fichier texte, je trouve ça un peu bizarre, je pense que j'aurai tendance à pousser vers du json complet (mais ce n'est pas forcement une bonne idée, ça va probablement alourdir ton traitement)
J'utilise pas énormément de json, mais de mémoire je faisais avec un mix de tableaux et d'objets (j'avais eu des conflits en n'essayant de ne faire qu'avec des objets (plus pratique pour accéder aux éléments).
Cela donnerait un truc du genre je crois :
let data = {
    "div": [
        {
            "img": {
                "src": "https://medias/images/uploads/clay-banks-original.webp",
                "alt": "Image représentant un phare en bord de mer",
                "description": "Yaquina Head, Newport, États-Unis.",
                "align": "right",
                "width": "2002",
                "height": "3000"
            }
        },
        {
            "p": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        },
        {
            "img": {
                "src": "/medias/images/uploads/clay-banks-original.webp",
                "description": "Yaquina Head, Newport, États-Unis.",
                "height": "3000",
                "align": "right",
                "width": "2002"
            }
        },
        {
            "p": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam."
        },
        {
            "img": {
                "src": "https://medias/images/uploads/clay-banks-original.webp",
                "align": "right",
                "width": "2002",
                "height": "3000"
            }
        }
    ]
}


Ensuite il faudrait faire des systèmes de boucles pour parcourir en entier et faire les bonnes fonctions qui reconstruise le tout
Salut Mathieuu,

En fait, le contenu n'est pas prédictible car il peut être fournit par un rédacteur/contributeur.

Je crée un CMS maison. En l'état les données peuvent être fournies en JSON. Comme la plupart des frameworks aujourd'hui, c'est le comportement par défaut (j'utilise Fastify). Perso je préfère faire la transformation des données avec l'enrobage HTML côté serveur (avec le moteur de template Pug).

Donc, dans le cas qui nous occupe ici il faut s'imaginer que nous sommes dans le corps d'un article, et je veux éviter au rédacteur la peine de devoir taper du code HTML pour mettre en forme son article, d'où l'idée des shortcodes. Je ne sais pas encore si j'utiliserais une syntaxe Markdown ou autre, mais quoi qu'il en soit il faudra que je fasse des transformations HTML pour coller aux conventions du site.
Bonsoir,

Si tu en as la possibilité, choisis un format connu et éprouvé comme markdown, auquel tu peux toujours ajouter quelques extensions si besoin, plutôt que de réinventer un format maison.

LE gros avantage d'utiliser un format connu, côté utilisateur, c'est le transfert de connaissance d'un outil à l'autre.
En plus, le format markdown est déjà reconnu comme étant sans doute un des plus simple à appréhender pour des rédacteurs qui ne sont peut-être pas tous hyper technophiles.

De ton côté, il existe sûrement déjà des dizaines de modules JavaScript npm qui parsent du markdown, dont certains bien maintenus. Ca te fait du code en moins à gérer.
Oui, ce sera certainement Markdown, et puis au final je ne veux pas que l'on puisse avoir trop de choix de design pour la composition des articles : je veux que l'article reste simple et donc éviter d'avoir trop d'options qui viennent "pourrir" le design du site par leurs hétéroclicités.

J'ai connu ça avec WordPress. Ah là là ! les rédacteurs... ils me faisaient de ces trucs...

D'ailleurs, pour ce dernier, il s'agira à terme de convertir un peu plus de mille articles. Je ne pourrais pas simplement faire un "HTML to Markdown" car WordPress prend en charge l'enveloppement des paragraphes et comme je me suis pas mal appuyé sur cette fonctionnalité WordPressienne (à tort sans doute) je n'ai quasiment pas de balises <p>. Je vais bien m'amuser le jour où je vais nettoyer mes data.

De toute façon, ce CMS, il est à un stade expérimental et, à priori, il ne dépassera guère le rayon de la confidentialité. L'API REST fonctionne sans problème, par contre je bute sur l'authentification. Ce sera sans doute l'objet d'une question prochaine sur le forum (ainsi que pour les rôles).
Petit retour au bout de 24h :

En définitive je manipule bien à l'aide du format JSON, mais je dispose les données du shortcode à plat, un peu comme avec mon shortcode d'origine, sauf que maintenant tous les paramètres doivent avoir un attribut. Ceci pour une lecture plus facile pour le contributeur. Je fais donc ceci :
{{img src="/medias/images/uploads/clay-banks-original.webp" description="Yaquina Head, Newport, États-Unis." align="right" width="2002" height="3000"}}

Au lieu de ceci (format que j'utilise toujours pour manipuler mes données, mais après un formatage via regex) :
{"img": {"src": "/medias/images/uploads/clay-banks-original.webp", "description": "Yaquina Head, Newport, États-Unis.", "align": "right", "width": "2002", "height": "3000"}}}

Donc, pour ce nouveau format de donnée, et afin que cela rende toujours le HTML décrit plus haut, je fais ceci :
(function imageShortcode() {
  data = data.replace(/{{\s*img\s(.*?)\s*}}/g, (match, imgData) => {
    const imgDataJson = `{${imgData.replace(/([a-zA-Z]*?)=(".*?")(?:\s*,*)/g, `"$1": $2,`).replace(/,\s*$/, '')}}`
    const { src, alt, description, width, height, align } = JSON.parse(imgDataJson) // @note Parsage via un format JSON pour faciliter la manipulation des données.
    const imgSrcset = `<source srcset="${src.replace(/-original\./, `-w600.`)}, ${src.replace(/-original\./, `-w1000.`)} 2x">`
    const classAlign = align === 'right' ? `-alignright` : `-alignleft`
    const figcaption = description ? `<figcaption>${description}</figcaption>` : ''
    return `<figure class="figure-image-focus-thumbnail${classAlign}"><picture>${imgSrcset}<img src="${src}" alt="${
      alt || description || `Une image`
    }" width="${width || 'auto'}" height="${height || 'auto'}"></picture>${figcaption}</figure>`
  })
})(data)

À terme il serait bien de prévoir une modale qui génère le shortcode à partir des uploads. Mais je n'en suis pas encore là.
Modifié par Olivier C (25 Feb 2024 - 15:35)
Un petit retour sur le markdown : j'ai bien implémenté cette solution au final, avec Marked.js. Maintenant je peux rédiger soit en HTML soit en markdown, soit les deux. Ce qui me permet de rester compatible avec mes anciennes sauvegardes.

Par contre, vu que ce fameux markdown n'est pas fait pour être implémenté avec du HTML complexe (coucou <figure>, <figcaption>, etc), mes shortcodes (accordéons, maps) restent tout à fait pertinents, mais markdown a parfois des effets de bords. Par exemple il wrap les balises <audio> et <video> dans un paragraphe...

J'ai trouvé la solution pour filtrer ce comportement, je refile l'astuce pour la postérité (et le moi du futur) :
(function customMarked() {
  const renderer = new marked.Renderer()
  // @note Remplacer la méthode de rendu des paragraphes afin d'ignorer les balises <audio> et <video>
  renderer.paragraph = function (text) {
    if (text.startsWith('<video') || text.startsWith('<audio')) return `<p>${text}</p>`
    return `<p>${text}</p>`
  }
  marked.setOptions({ renderer })
})()

Modifié par Olivier C (25 Mar 2024 - 14:58)