Django: insérer média dans un article en markdown

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je fais mes premier pas avec Django avec un petit site perso. Dans le cahier des charges, je veux que la rédaction d’un article puisse se faire entièrement en markdown. La flemme de coder un éditeur graphique (qui nécessitera en plus que je me mette au Javascript) pour au final un truc moins pratique à utiliser que du markdown. J’ai installé l’extension django-markdownx. Ça marche très bien.

Cela dit, j’ai un problème lorsque je veux insérer des médias dans un article :

J’ai commencé par les images. L’extension autorise la syntaxe ![…](… "…"). Ensuite j’ai créé un modèle image dans la base de données qui me permet d’uploader directement les images, avec un override de la fonction save() qui me permet d’appliquer une petite optimisation sur ladite image, avant de la stocquer dans le répertoire /media/images.

Là se pose un premier problème. Comment faire référence en markdown à une image stockée dans la BDD ? Actuellement, je contourne le problème en utilisant directement le lien « /media/images/mon_images.webp » dans la balise markdown. Ce qui dans l’absolu me convient, même si c’est un peu verbeux.

Par contre du coup ma table Image ne me sert à rien : pas de sélection des images d’un article, ou d’articles utilisant une image donnée, pas de mise à jour de l’article si je ré-upload une image modifié dans la table, etc.

Je peux bien sûr rajouter un champ ManyToMany dans le modèle Article, mais avec infraction au principe DRY, et risque d’incohérence (et toujours le problème de la mise à jour automatique de l’article).

Ensuite viens le cas des vidéos et des sons : il n’y a pas de syntaxe markdown pour les balises <video> et <audio>.

Pareil, possibilité d’écrire les balises en toutes lettres. C’est moche et verbeux. Possibilité de créer un parseur custom et d’y faire appel dans le save() de la classe Article. Ça passe. Mais ensuite on se retrouve avec le même problème que pour la classe/table Image. Sauf qu’en plus il n’y a pas de champ VideoField et AudioField dans Django.

Je peux donc remplacer la classe Image avec son ImageField par une classe Media avec un FileField, ce qui encore une fois ne résout pas les problèmes de liens entre articles et médias et oblige en plus à une détection plus poussée du média en question.

Bref :

Je ne trouve pas de solution qui me parait propre. Quoique je fasse, j’ai toujours l’impression de bidouiller. C’est pourquoi avant de me lancer tête baisser dans mes bidouillage, je préfère poser la question sur un forum à des développeurs plus expérimentés :

Si vous étiez face à cette problématique, comment vous y prendriez-vous ?

Merci à ceux qui ont pris le temps de me lire.

EDIT :

Bon pour les images en fait, django-markdownx permet le glissé-déposé. Il faut juste corriger un bug en attendant que la prochaine release sorte.

Pour le moment je l’ai fait à la main en éditant directement le fichier venv/lib/python3.10/site-packages/markdownx/view.py.

Question 1 : y-a-t-il une méthode plus propre pour patcher le module ? Je ne suis pas certain que ça fasse partie des bonnes pratiques d’éditer directement le fichier du module.

ensuite, je pense que l’idéal serait d’overrider le parseur de markdownx pour pouvoir glisser-déposer n’importe quel fichier, puis adapter les balises en fonction de l’extension.

Question 2 : comment on fait ça ?

+0 -0

Salut,

Par rapport à ton problème initial, le modèle ImageField ne te permet pas directement d’obtenir l’URL d’un fichier image importé ?

Ensuite concernant ta première question c’est assez difficile et c’est au cas par cas. Le plus simple serait de maintenir un fork de la lib en question qui résoudrait le bug (le temps qu’il soit vraiment résolu dans le projet initial), et d’installer ce paquet à la place de la lib voulue.

Sinon il reste possible d’aller patcher à la volée (soit truquer les répertoires d’imports pour que ton patch ait la priorité, soit faire de la magie pour que le fichier soit modifié à la lecture) mais c’est casse-gueule. De même que ta modification directe du fichier dans l’env virtuel parce qu’elle n’affecte que l’env courant et pétera à la première mise à jour.

Pour ta seconde question je n’ai malheureusement pas de réponse à t’apporter et j’imagine qu’il faudrait se plonger dans le code de markdownx pour voir comment ajouter le traitement d’une nouvelle balise : peut-être que tu as de la chance et que c’est configurable, ou alors peut-être qu’il va là aussi falloir forker et patcher le code dans le sens que tu veux.

Sinon tu as peut-être d’autres libs qui peuvent faire ça, je pense par exemple à zmarkdown qui est utilisée ici mais je ne sais pas si ça peut répondre à ton besoin.

Salut, et merci de ta réponse.

Alors j’ai réussi à me débarbouiller en utilisant l’api d’extension de markdown.

Pour ceux que ça intéresse voici le code :

# my_app/markdown/extensions/media.py

from markdown.blockprocessors import BlockProcessor, ParagraphProcessor
from markdown.treeprocessors import Treeprocessor
from markdown.inlinepatterns import LinkInlineProcessor, ReferenceInlineProcessor
from markdown.extensions import Extension

import xml.etree.ElementTree as etree
import re


class FigureProcessor(BlockProcessor):
    """ Process Figures. """
    RE = re.compile(
        r'^[ ]{0,3}!\[(.*)]\(.*( ".*?")?\)$',
        re.MULTILINE
    )

    def test(self, parent, block):
        return bool(self.RE.match(block))

    def run(self, parent, blocks):
        block = blocks.pop(0)
        f = etree.SubElement(parent, 'figure', attrib={"class": "media"})
        f.text = block.lstrip()


class FigcaptionTreeProcessor(Treeprocessor):
    def run(self, root):
        for f in root.findall("./figure[@class='media']"):
            if len(f) == 1 and f[0].get("title"):
                caption = etree.SubElement(f, "figcaption")
                caption.text = f[0].get("title")


class MediaInlineProcessor(LinkInlineProcessor):
    """ Return a img/video/audio element from the given match. """

    def handleMatch(self, m, data):
        text, index, handled = self.getText(data, m.end(0))
        if not handled:
            return None, None, None

        src, title, index, handled = self.getLink(data, index)
        if not handled:
            return None, None, None

        ext = src.split(".")[-1]

        if ext in ["jpg", "jpeg", "png", "gif", "webp"]:
            el = etree.Element("img")

        elif ext in ["mp4", "webm", "avi"]:
            el = etree.Element("video")
            el.attrib = {"controls":"true", "width":"1250"}

        elif ext in ["mp3", "ogg", "opus"]:
            el = etree.Element("audio")

        else:
            el = etree.Element("video")
            el.attrib = {"controls": "", "width": "1250"}

        el.set("src", src)

        if title is not None:
            el.set("title", title)

        el.set('alt', self.unescape(text))
        return el, m.start(0), index


class MediaReferenceInlineProcessor(ReferenceInlineProcessor):
    """ Match to a stored reference and return img element. """
    def makeTag(self, href, title, text):
        ext = href.split(".")[-1]

        if ext in ["jpg", "jpeg", "png", "gif", "webp"]:
            el = etree.Element("img")

        elif ext in ["mp4", "webm", "avi"]:
            el = etree.Element("video")

        elif ext in ["mp3", "ogg", "opus"]:
            el = etree.Element("audio")

        else:
            el = etree.Element("img")

        el.set("src", href)
        if title:
            el.set("title", title)
        el.set("alt", self.unescape(text))
        return el


class MediaExtension(Extension):
    def extendMarkdown(self, md):
        md.inlinePatterns.deregister('image_link')
        md.inlinePatterns.deregister('image_reference')
        md.parser.blockprocessors.deregister('paragraph')
        md.parser.blockprocessors.register(
            FigureProcessor(md.parser),
            'figure',
            10)
        md.parser.blockprocessors.register(
            ParagraphProcessor(md.parser),
            'paragraph',
            10)
        md.treeprocessors.register(FigcaptionTreeProcessor(md),
                                   'figcaption',
                                   10)
        # ![alttxt](http://x.com/) or ![alttxt](<http://x.com/>)
        MEDIA_LINK_RE = r'\!\['
        # ![alt text][2]
        MEDIA_REFERENCE_RE = MEDIA_LINK_RE
        md.inlinePatterns.register(
            MediaInlineProcessor(MEDIA_LINK_RE, md),
            'media_link',
            150)
        md.inlinePatterns.register(
            MediaReferenceInlineProcessor(MEDIA_REFERENCE_RE, md),
            'media_reference',
            140)


def makeExtension(**kwargs):  # pragma: no cover
    return MediaExtension(**kwargs)
# setting.py
[…]
MARKDOWNX_MARKDOWN_EXTENSIONS = [
    'markdown.extensions.extra',
    'markdown.extensions.codehilite',
    'my_app.markdown.extensions.media',  # Bien penser à créer un fichier __init__.py dans le dossier extensions pour que ça fonctionne.
]
[…]

J’en ai profité pour remplacer la balise parente <p> par une balise <figure> lorsqu’il s’agit d’une image isolé et j’ai ajouté une balise <figcaption> contenant le titre de l’image lorsque celle-ci en possède un. N’hésitez pas si avez à y redire sur ce code.

Ensuite :

Par rapport à ton problème initial, le modèle ImageField ne te permet pas directement d’obtenir l’URL d’un fichier image importé ?

Si. Mais si je fait ![alt](l/url/en/question.jpg "titre"), ça ne crée pas de lien relationnel entre l’image et l’article. En fait je pense que je vais me contenter de ça et supprimer complètement la classe image.

Ça va entraîner de nouvelle questions (comment je fais pour traiter automatiquement mes images après le glisser/déposer, par exemple ?). Je pense que je vais avoir encore besoin de ce topic.

Ensuite concernant ta première question c’est assez difficile et c’est au cas par cas. Le plus simple serait de maintenir un fork de la lib en question qui résoudrait le bug (le temps qu’il soit vraiment résolu dans le projet initial), et d’installer ce paquet à la place de la lib voulue.

J’avoue que je ne suis pas certain de savoir comment faire ça.

Sinon il reste possible d’aller patcher à la volée (soit truquer les répertoires d’imports pour que ton patch ait la priorité, soit faire de la magie pour que le fichier soit modifié à la lecture) mais c’est casse-gueule

Idem.

De même que ta modification directe du fichier dans l’env virtuel parce qu’elle n’affecte que l’env courant et pétera à la première mise à jour.

Je me doutait que ça serait comme ça. Bon, je pense que je vais rester là dessus pour le moment. D’après le topic sur Github, le bug est censé être corrigé dans la prochaine version. Je suis pas certain que ça vaille le coup de me prendre la tête avec ça s’il suffit d’attendre.

Salut,

Visiblement le repository n’est pas très actif… Par contre, il y a une version beta qui a été sortie en Janvier (!), et disponible sur PyPI. Si celle-ci fonctionne pour ce que tu veux faire, c’est sûrement un bon pari plutôt que bricoler des patchs.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte