Jouons un peu avec Jinja2 et YAML

Publié le 07/07/2015

Bon, et si je voulais développer mon propre générateur de sites statiques ? Pas que ce soit nécessaire, car il y en a des dizaines de centaines de milliers (+une petite dizaine chaque jour), juste que j'en ai envie. Et aussi pour découvrir VueJS 😁.

https://vuejs.org/[VueJS] se définit comme un framework progressif. Contrairement à un framework type React ou Angular, il est effectivement possible d'intégrer un morceau du framework à certains endroits uniquement, pour faire évoluer une application Web existante.

Les idées de base sont les suivantes:

  • Utiliser markdown et ses extensions officielles pour la génération de code HTML.
  • Passer le tout dans Pygments pour la coloration de code
  • Ajouter une couche de Typogrify pour gérer quelques cas particuliers de mises en forme.
  • Sérialiser toutes ces informations en JSON, pour qu'elles soient consommées par une interface "moderne" (ie. Single Page Application + ajax + framework web à la mode).

Côté backend

from jinja2 import Environment, PackageLoader, select_autoescape

from models import Site

env = Environment(
    loader=PackageLoader('grnx', 'templates'),
    autoescape=select_autoescape(['html'])
)

if __name__ == "__main__":
    root = Site('content')
    root.serialize()

    template = env.get_template('single.html')
    for article in root.articles:
        print(template.render(article.__dict__))

import datetime
import json
import re
import os

import yaml


RE_HEADER = re.compile('---((.)?(\n)?)*---')

date_handler = lambda obj: (
    obj.isoformat()
    if isinstance(obj, datetime.datetime)
    or isinstance(obj, datetime.date)
    else None
)

def json_handler(obj):
    """Handles the JSON object serializer.

    Returns:
        The iso format if the object is a date.
        The __dict__ attribute for any other JSON serializable object.

    Excepts:
        TypeError when object is not JSON serialisable.
    """

    if hasattr(obj, 'isoformat'):
        return obj.isoformat()
    elif obj.__dict__:
        return obj.__dict__
    else:
        raise TypeError(
            'Object of type %s with value %s is not JSON serializable'
            % (type(obj), repr(obj))
        )


class Content(object):

    def __init__(self, filepath):

        with open(filepath, 'r') as f:
            try:
                file_content = f.read()
                print(file_content)
                regex_result = RE_HEADER.search(file_content)
                if regex_result:
                    header = file_content[regex_result.start():regex_result.end()-3]
                    self.properties = yaml.load(header)
                    headers = self.properties
                    print('headers: ' + str(headers))

                    self.published_date = headers.pop('Date', None)
                    self.last_modified_date = headers.pop('LastModified', self.published_date)
                    self.title = headers.pop('Title', None)
                    self.slug = headers.pop('Slug', None)
                    self.path = filepath
            except yaml.YAMLError as error:
                print(error)


class Site(object):
    """Represents a Site object.

    Args:
        articles: containt all articles.
        metadata: hum. I don't remember what I would store in this property.
        taxonomies: the taxonomies we find for this site. Tags, categories, ...

    """

    def __init__(self, current_path):
        self.articles = []
        self.metadata = {}
        self.taxonomies = {}

        for directory, dirnames, files in os.walk(current_path):
            for file in files:
                if file.endswith(".md"):
                    self.manage_article(directory, file)

    def manage_article(self, directory, file):
        article = Content(os.path.join(directory, file))
        self.articles.append(article)

        for taxonomy in article.properties:
            if taxonomy not in self.taxonomies:
                self.taxonomies[taxonomy] = []

            self.taxonomies[taxonomy].append(article.slug)

    def to_json(self):
        """Serialize the content of the current structure to JSON format."""

        return json.dumps(self, default=json_handler, sort_keys=True, indent=4)

    def serialize(self):
        """Serialize the current files structure to index.json"""

        with open('index.json', 'w') as json_serialized_file:
            json_serialized_file.write(self.to_json())

Après relecture, il y a teeellement d'améliorations possibles... 😢 que ce ne sera sans doute jamais terminé. Je garde le code sous le coude, au cas où.

Côte frontend (que je ne réaliserai pas)

Voir ici pour un retour d'expérience.

En gros, pour l'environnement technique, on a:

  • ES6 (et même plus, parce qu'on utilise async/await)
  • Babel pour la compatibilité
  • Webpack pour lier tout ça
  • Vue.js comme framework avec vue-router pour le routage et vuex pour la gestion d'états type flux (et donc vue-cli 3* pour gérer une bonne partie des aspects)
  • Axios pour la communication réseau
  • Jest pour les tests unitaires
  • Gitlab CI pour l'intégration continue
  • eslint en mode paranoïaque pour les bonnes pratiques et le formatage du code (c'est fou ce qu'on peut apprendre en* lisant les erreurs de lint)

... Pas vraiment le temps de rentrer dans tout ça pour le moment ⚔