2024-11-14 13:32:56 +01:00
|
|
|
#!/bin/python3
|
2024-11-15 15:56:11 +01:00
|
|
|
import os
|
2024-11-14 13:32:56 +01:00
|
|
|
import re
|
2024-11-15 15:56:11 +01:00
|
|
|
import shutil
|
|
|
|
from datetime import datetime
|
2025-02-19 16:24:20 +01:00
|
|
|
import pypandoc
|
2025-02-27 16:18:47 +01:00
|
|
|
import subprocess
|
|
|
|
import tempfile
|
|
|
|
from md2gemini import md2gemini
|
2025-02-28 00:40:13 +01:00
|
|
|
import locale
|
2024-11-15 15:56:11 +01:00
|
|
|
|
|
|
|
from website_config import *
|
2024-11-14 13:32:56 +01:00
|
|
|
|
|
|
|
# this path should be customized
|
|
|
|
org_roam_dir: str = '/home/tykayn/Nextcloud/textes/orgmode/org-roam/'
|
|
|
|
|
2024-11-15 15:56:11 +01:00
|
|
|
# Trouver l'identifiant OrgROAM
|
2024-11-14 13:32:56 +01:00
|
|
|
pattern_roam_id_search = r':ID:(?:\s+)?([a-zA-Z0-9-]+)'
|
2024-11-15 15:56:11 +01:00
|
|
|
# Expression régulière pour extraire la date et le slug du nom de fichier org
|
2024-11-19 13:49:39 +01:00
|
|
|
regex = r"^([a-zA-Z0-9_-]+)\_\_(-[a-zA-Z0-9_-]+)\.org$"
|
2024-11-15 15:56:11 +01:00
|
|
|
# Recherche de date de création du fichier org-roam dans un article gemini
|
2024-11-19 13:49:39 +01:00
|
|
|
regex_orgroam = regex
|
2024-11-15 15:56:11 +01:00
|
|
|
|
|
|
|
# show_logs=True
|
|
|
|
show_logs = global_config["show_logs"]
|
|
|
|
|
|
|
|
|
|
|
|
def mylog(*content):
|
|
|
|
"""Fonction qui imprime tous les arguments passés selon le niveau de debug souhaité."""
|
|
|
|
if show_logs:
|
2024-11-19 13:49:39 +01:00
|
|
|
for c in content:
|
|
|
|
print(' ',c)
|
2024-11-15 15:56:11 +01:00
|
|
|
|
|
|
|
|
|
|
|
def trouver_nom_article(fichier_org, blog_name, format="html"):
|
2025-02-19 16:24:20 +01:00
|
|
|
# mylog('fichier_org, ', fichier_org)
|
2024-11-15 15:56:11 +01:00
|
|
|
with open(fichier_org, 'r') as file:
|
|
|
|
lignes = file.readlines()
|
|
|
|
|
|
|
|
nom_article = ''
|
|
|
|
|
2025-01-29 17:37:29 +01:00
|
|
|
# mylog('trouver_nom_article format', format)
|
2024-11-15 15:56:11 +01:00
|
|
|
# Expressions régulières pour trouver les titres de niveau 1 et 2
|
|
|
|
if format == 'html':
|
|
|
|
titre_niveau_1 = r'<h1\s+(?:id|data-created)="[^"]*">(.*?)</h1>'
|
|
|
|
titre_niveau_2 = r'^\<h2.*?\>(.+)\<\/h2\>$'
|
|
|
|
else:
|
|
|
|
titre_niveau_1 = r'^\*+ (.+)$'
|
|
|
|
titre_niveau_2 = r'^\*\*+ (.+)$'
|
|
|
|
|
|
|
|
# Itérer sur les lignes du fichier
|
|
|
|
for ligne in lignes:
|
|
|
|
# Rechercher un titre de niveau 1
|
|
|
|
titre_niveau_1_match = re.match(titre_niveau_1, ligne)
|
|
|
|
if titre_niveau_1_match:
|
|
|
|
titre_niveau_1_texte = titre_niveau_1_match.group(1)
|
|
|
|
if titre_niveau_1_texte.lower() != "article" and titre_niveau_1_texte.lower() != "liens":
|
|
|
|
nom_article = titre_niveau_1_texte
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
# Si le premier titre de niveau 1 est "Article", rechercher le premier titre de niveau 2
|
|
|
|
titre_niveau_2_match = re.match(titre_niveau_2, ligne)
|
|
|
|
if titre_niveau_2_match:
|
|
|
|
nom_article = titre_niveau_2_match.group(1)
|
|
|
|
break
|
2025-01-29 17:37:29 +01:00
|
|
|
# mylog(f"Nom de l'article : {nom_article}")
|
2024-11-15 15:56:11 +01:00
|
|
|
|
|
|
|
return nom_article.replace(blog_name + '_', '').replace('_', ' ')
|
|
|
|
|
2025-02-23 15:50:56 +01:00
|
|
|
def find_org_roam_id(content):
|
|
|
|
match = re.search(pattern_roam_id_search, content)
|
|
|
|
if match:
|
|
|
|
return match.group(1)
|
|
|
|
return None
|
|
|
|
|
|
|
|
def get_blog_template_conf(blogname) -> dict:
|
|
|
|
"""
|
|
|
|
Retourne la configuration du blog spécifié.
|
|
|
|
|
|
|
|
:param blogname: Nom du blog (str).
|
|
|
|
:return: Configuration du blog (dict).
|
|
|
|
"""
|
|
|
|
if blogname not in configs_sites:
|
|
|
|
return default_config
|
|
|
|
else:
|
|
|
|
return configs_sites[blogname]
|
2024-11-15 15:56:11 +01:00
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
def find_year_and_slug_on_filename(filename):
|
2025-02-27 19:34:49 +01:00
|
|
|
# print(f"Traitement du fichier: {filename}") # Debug
|
2025-02-27 16:18:47 +01:00
|
|
|
try:
|
|
|
|
# Supposons que le format attendu est "YYYYMMDDHHMMSS-slug.org"
|
|
|
|
date_str = filename[:14] # Prend les 14 premiers caractères pour la date
|
|
|
|
annee = date_str[:4] # Prend les 4 premiers caractères pour l'année
|
|
|
|
|
|
|
|
# Gestion plus robuste du slug
|
2025-05-13 10:41:40 +02:00
|
|
|
if '__' in filename:
|
|
|
|
slug = filename.split('__')[1].replace('.org', '')
|
2025-02-27 16:18:47 +01:00
|
|
|
else:
|
|
|
|
slug = filename.replace('.org', '')
|
|
|
|
|
|
|
|
return date_str, annee, slug
|
|
|
|
except Exception as e:
|
|
|
|
print(f"Format de fichier non standard: {filename}")
|
|
|
|
return None, None, filename.replace('.org', '')
|
2024-11-15 15:56:11 +01:00
|
|
|
|
|
|
|
|
|
|
|
def enlever_premier_tiret_ou_underscore(chaîne):
|
|
|
|
if chaîne.startswith('-') or chaîne.startswith('_'):
|
|
|
|
chaîne = chaîne[1:]
|
|
|
|
return chaîne
|
|
|
|
|
|
|
|
|
|
|
|
# création des dossiers intermédiaires s'il y en a
|
|
|
|
# déplace le fichier dans le dossier spécifié
|
|
|
|
def create_path_folders_and_move_file(path, file):
|
|
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
|
|
|
|
|
|
shutil.move(file, path)
|
|
|
|
|
|
|
|
|
|
|
|
def get_files_list_of_folder(folder_path):
|
|
|
|
# Vérifie si le dossier existe
|
|
|
|
if not os.path.exists(folder_path):
|
|
|
|
print(f" ------------ build_indexes: Erreur : Le dossier '{folder_path}' n'existe pas.")
|
|
|
|
return
|
|
|
|
mylog('----------- get_files_list_of_folder: folder_path : ', folder_path)
|
|
|
|
# Liste les fichiers articles, trie par nom décroissant
|
|
|
|
try:
|
|
|
|
fichiers_md = sorted(
|
2024-11-15 16:24:31 +01:00
|
|
|
[f.replace('.' + global_config['source_files_extension'], '.gmi') for f in os.listdir(folder_path) if
|
|
|
|
f.endswith(global_config['source_files_extension'])], reverse=True)
|
2025-02-27 19:34:49 +01:00
|
|
|
# print('fichiers trouvés:', len(fichiers_md))
|
2024-11-15 15:56:11 +01:00
|
|
|
return fichiers_md
|
|
|
|
except OSError as e:
|
|
|
|
print(f" ------------ build_indexes: Erreur lors de la lecture du dossier : {e}")
|
|
|
|
return
|
2024-11-14 13:32:56 +01:00
|
|
|
|
2024-11-14 16:22:34 +01:00
|
|
|
|
2024-11-14 13:32:56 +01:00
|
|
|
def get_id_of_roam_note_content(content):
|
|
|
|
match = re.search(pattern_roam_id_search, content)
|
|
|
|
if match:
|
|
|
|
return match.group(1)
|
|
|
|
return None
|
|
|
|
|
2024-11-14 16:22:34 +01:00
|
|
|
|
2024-11-14 13:32:56 +01:00
|
|
|
def find_first_level1_title(content):
|
2025-02-27 19:34:49 +01:00
|
|
|
pattern = r'^\*\s(.+)$'
|
2024-11-14 13:32:56 +01:00
|
|
|
match = re.search(pattern, content, re.MULTILINE)
|
|
|
|
if match:
|
|
|
|
if match.group(1) != 'Article':
|
|
|
|
return match.group(1)
|
|
|
|
else:
|
2025-02-27 19:34:49 +01:00
|
|
|
pattern = r'^\*\*\s(.+)$'
|
2024-11-14 13:32:56 +01:00
|
|
|
match = re.search(pattern, content, re.MULTILINE)
|
|
|
|
if match:
|
|
|
|
return match.group(1)
|
|
|
|
return None
|
|
|
|
|
2024-11-18 11:18:50 +01:00
|
|
|
def find_extract_in_content_org(org_content):
|
|
|
|
# Supprimer les lignes qui commencent par #+
|
|
|
|
org_content = re.sub(r'^\s*#\+.*\n', '', org_content, flags=re.MULTILINE)
|
|
|
|
|
|
|
|
# Supprimer les sections de logbook
|
|
|
|
org_content = re.sub(r'^\*\* Logbook\n.*?(?=\*\* |\Z)', '', org_content, flags=re.DOTALL | re.MULTILINE)
|
|
|
|
|
|
|
|
# Supprimer les propriétés
|
|
|
|
org_content = re.sub(r'^:PROPERTIES:\n.*?:END:\n', '', org_content, flags=re.DOTALL | re.MULTILINE)
|
|
|
|
|
|
|
|
# Supprimer les lignes vides supplémentaires
|
|
|
|
org_content = re.sub(r'\n\s*\n+', '\n', org_content)
|
|
|
|
|
|
|
|
# Supprimer les espaces en début et fin de chaque ligne
|
|
|
|
org_content = '\n'.join(line.strip() for line in org_content.splitlines())
|
|
|
|
|
|
|
|
# Supprimer les espaces en début et fin du contenu final
|
|
|
|
return org_content.strip()
|
2024-11-14 16:22:34 +01:00
|
|
|
|
|
|
|
def extract_body_content(html_content):
|
2024-11-15 15:56:11 +01:00
|
|
|
pattern = r'<body.*?>(.*?)</body>'
|
2024-11-14 16:22:34 +01:00
|
|
|
match = re.search(pattern, html_content, re.DOTALL)
|
|
|
|
if match:
|
|
|
|
return match.group(1)
|
|
|
|
else:
|
2024-11-15 01:45:11 +01:00
|
|
|
print('---- extract_body_content : no body found in this html')
|
|
|
|
return html_content
|
2024-11-14 16:22:34 +01:00
|
|
|
|
2024-11-20 00:24:09 +01:00
|
|
|
def add_tags_from_content(tags=None, file_content="", words_to_check=None):
|
|
|
|
"""
|
|
|
|
Ajoute des tags à l'ensemble `tags` si les mots correspondants sont trouvés dans le contenu du fichier.
|
|
|
|
|
|
|
|
:param tags: Ensemble de tags (set). Si None, un nouvel ensemble est créé (type set, optionnel).
|
|
|
|
:param file_content: Contenu du fichier (str).
|
|
|
|
:param words_to_check: Liste de mots à repérer (list). Si None, une liste vide est utilisée (type list, optionnel).
|
|
|
|
:return: Ensemble de tags mis à jour (set).
|
|
|
|
"""
|
|
|
|
# Initialiser l'ensemble tags s'il est None
|
|
|
|
if tags is None:
|
|
|
|
tags = set()
|
|
|
|
|
|
|
|
# Initialiser la liste words_to_check s'il est None
|
|
|
|
if words_to_check is None:
|
|
|
|
words_to_check = []
|
|
|
|
|
|
|
|
# Convertir le contenu du fichier en minuscules pour une recherche insensible à la casse
|
|
|
|
file_content_lower = file_content.lower()
|
|
|
|
|
|
|
|
# Parcourir chaque mot à vérifier
|
|
|
|
for word in words_to_check:
|
|
|
|
# Vérifier si le mot est présent dans le contenu du fichier
|
2025-02-20 15:31:44 +01:00
|
|
|
# Chercher une correspondance sans mettre en lowercase si le tag est en majuscule, c'est sans doute un acronyme/sigle.
|
|
|
|
if word.isupper():
|
|
|
|
if word in file_content:
|
|
|
|
tags.add(word)
|
|
|
|
else:
|
|
|
|
if word.lower() in file_content_lower:
|
|
|
|
tags.add(word)
|
2024-11-20 00:24:09 +01:00
|
|
|
|
|
|
|
return tags
|
2025-02-27 16:18:47 +01:00
|
|
|
# Variable globale pour stocker les fichiers sans tags
|
|
|
|
untagged_files = []
|
|
|
|
|
|
|
|
def save_untagged_files(output_file="sources/site_web/build/articles_without_tags.json"):
|
|
|
|
"""
|
|
|
|
Sauvegarde la liste des fichiers sans tags dans un fichier JSON.
|
|
|
|
|
|
|
|
:param output_file: Chemin du fichier JSON de sortie
|
|
|
|
"""
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
|
|
|
|
# Créer le dossier de sortie si nécessaire
|
|
|
|
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
|
|
|
|
|
|
|
print('save_untagged_files', len(untagged_files))
|
2025-05-12 23:01:56 +02:00
|
|
|
for f in untagged_files:
|
|
|
|
print('- ', f)
|
2025-02-27 16:18:47 +01:00
|
|
|
# Sauvegarder la liste dans le fichier JSON
|
|
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
|
|
json.dump(untagged_files, f, ensure_ascii=False, indent=4)
|
2024-11-20 00:24:09 +01:00
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
|
|
|
|
def extract_tags_from_file(file_path, excluded_tags, auto_detected_tags_list=global_config['auto_tag_terms']):
|
2024-11-20 00:24:09 +01:00
|
|
|
tags = set()
|
|
|
|
with open(file_path, 'r', encoding='utf-8') as file_content:
|
|
|
|
tag_found = False
|
|
|
|
for line in file_content:
|
2025-02-27 16:18:47 +01:00
|
|
|
if global_config['automatic_tagging_enabled'] and len(auto_detected_tags_list) > 0:
|
|
|
|
tags = add_tags_from_content(tags, line, auto_detected_tags_list)
|
2024-11-20 00:24:09 +01:00
|
|
|
# Check for orgmode tags :tag1:tag2:
|
2025-02-27 16:18:47 +01:00
|
|
|
if global_config.get('automatic_tagging_org_files', True):
|
|
|
|
if ':' in line:
|
|
|
|
for word in line.split():
|
|
|
|
if len(word) > 1 and word.startswith(':') and word.endswith(':'):
|
|
|
|
tag = word[1:-1]
|
|
|
|
if tag not in excluded_tags:
|
|
|
|
tags.add(tag)
|
2024-11-20 00:24:09 +01:00
|
|
|
tag_found = True
|
|
|
|
# Check for #+tags: tag1,tag2
|
|
|
|
if line.startswith('#+tags:'):
|
|
|
|
for tag in line[len('#+tags:'):].split(','):
|
|
|
|
tag = tag.strip()
|
|
|
|
if tag and tag not in excluded_tags:
|
|
|
|
tags.add(tag)
|
|
|
|
tag_found = True
|
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
if not tag_found:
|
|
|
|
untagged_files.append(file_path)
|
2025-02-19 22:39:11 +01:00
|
|
|
# print('no tag in the article', file_path)
|
2024-11-20 00:24:09 +01:00
|
|
|
return tags
|
2024-11-14 16:22:34 +01:00
|
|
|
|
|
|
|
def remove_properties_section(text):
|
|
|
|
pattern = r"<h1 id=\"article\">Article</h1>.+?</ul>"
|
|
|
|
replacement = ""
|
|
|
|
return re.sub(pattern, replacement, text, flags=re.DOTALL)
|
|
|
|
|
|
|
|
|
|
|
|
def remove_article_head_properties_orgmode(text):
|
|
|
|
pattern = r":PROPERTIES:.+?:END:"
|
|
|
|
replacement = ""
|
|
|
|
return re.sub(pattern, replacement, text, flags=re.DOTALL)
|
|
|
|
|
|
|
|
|
|
|
|
def remove_hint_html(text):
|
|
|
|
pattern = r"<p>ceci<sub>estduhtml</sub></p>"
|
|
|
|
replacement = ""
|
|
|
|
return re.sub(pattern, replacement, text, flags=re.DOTALL)
|
2024-11-15 16:24:31 +01:00
|
|
|
|
|
|
|
|
2025-02-27 22:59:57 +01:00
|
|
|
def slugify_title(title):
|
2024-11-15 23:55:20 +01:00
|
|
|
"""
|
2025-02-27 22:59:57 +01:00
|
|
|
Convertit un titre en slug URL-friendly en conservant les accents.
|
|
|
|
- Convertit en minuscules
|
|
|
|
- Conserve les accents francophones
|
|
|
|
- Remplace les caractères spéciaux par des tirets
|
|
|
|
- Supprime les tirets multiples
|
|
|
|
|
|
|
|
Args:
|
|
|
|
title (str): Le titre à convertir
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
str: Le slug généré
|
2024-11-15 23:55:20 +01:00
|
|
|
"""
|
2025-02-27 22:59:57 +01:00
|
|
|
# Conversion en minuscules
|
|
|
|
title = title.lower()
|
|
|
|
|
|
|
|
# Liste des caractères autorisés (inclut les accents francophones)
|
|
|
|
# On garde a-z, 0-9, les accents français, les tirets
|
|
|
|
allowed_chars = r'[^a-zàâäéèêëîïôöùûüçñ0-9-]'
|
|
|
|
|
|
|
|
# Remplacer les caractères non autorisés par des tirets
|
|
|
|
title = re.sub(allowed_chars, '-', title)
|
|
|
|
|
|
|
|
# Supprimer les tirets en début et fin
|
|
|
|
title = title.strip('-')
|
|
|
|
|
|
|
|
# Remplacer les tirets multiples par un seul
|
|
|
|
title = re.sub(r'-+', '-', title)
|
|
|
|
|
|
|
|
return title
|
2024-11-15 23:55:20 +01:00
|
|
|
|
2024-11-18 11:18:50 +01:00
|
|
|
def find_slug_in_file_basename(file_basename) -> str:
|
2024-11-15 16:24:31 +01:00
|
|
|
"""
|
|
|
|
Extrait l'année et le slug du nom de fichier selon le format spécifié.
|
|
|
|
|
|
|
|
:param file_basename: Nom de fichier (str).
|
|
|
|
:return: Tuple contenant l'année et le slug (année, slug) ou None si non trouvé.
|
|
|
|
"""
|
2024-11-19 23:50:42 +01:00
|
|
|
pattern = regex_orgroam
|
2024-11-15 16:24:31 +01:00
|
|
|
match = re.match(pattern, file_basename)
|
|
|
|
if match:
|
|
|
|
year = match.group(1)
|
|
|
|
slug = match.group(2)
|
2024-11-15 23:55:20 +01:00
|
|
|
# prendre la partie finale du nom du fichier
|
|
|
|
splitted = slug.split('_')
|
|
|
|
# print('len(splitted)', len(splitted), splitted)
|
|
|
|
if len(splitted) > 1:
|
|
|
|
slug = splitted[len(splitted)-1]
|
|
|
|
|
|
|
|
slug=enlever_premier_tiret_ou_underscore(slug)
|
2024-11-15 16:24:31 +01:00
|
|
|
|
2024-11-15 23:55:20 +01:00
|
|
|
slug = f"{year}/{slug}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return slug
|
2024-11-15 16:24:31 +01:00
|
|
|
return None
|
2024-11-15 23:55:20 +01:00
|
|
|
|
2025-02-23 16:59:59 +01:00
|
|
|
def get_stats_on_all_websites():
|
|
|
|
"""
|
|
|
|
Retourne des statistiques sur tous les sites web dans le dossier sources/.
|
|
|
|
Pour chaque site, compte le nombre d'articles .org et trouve l'article le plus récent.
|
|
|
|
|
|
|
|
:return: Dictionnaire avec les stats par site
|
|
|
|
"""
|
|
|
|
stats = {}
|
|
|
|
base_dir = "sources"
|
|
|
|
|
|
|
|
# Parcourir tous les dossiers de sites dans sources/
|
|
|
|
for site in os.listdir(base_dir):
|
|
|
|
site_path = os.path.join(base_dir, site)
|
|
|
|
|
|
|
|
if not os.path.isdir(site_path):
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Initialiser les stats pour ce site
|
|
|
|
stats[site] = {
|
|
|
|
'nb_articles': 0,
|
|
|
|
'nb_mots': 0,
|
|
|
|
'dernier_article': None,
|
|
|
|
'date_dernier_article': None
|
|
|
|
}
|
|
|
|
|
|
|
|
# Chercher les articles .org dans lang_fr et lang_en
|
|
|
|
for lang in ['lang_fr', 'lang_en']:
|
|
|
|
lang_path = os.path.join(site_path, lang)
|
|
|
|
|
|
|
|
if not os.path.exists(lang_path):
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Lister tous les fichiers .org
|
|
|
|
org_files = [f for f in os.listdir(lang_path) if f.endswith('.org')]
|
|
|
|
stats[site]['nb_articles'] += len(org_files)
|
|
|
|
# Calculer le nombre total de mots pour ce dossier de langue
|
|
|
|
total_mots = 0
|
|
|
|
for org_file in org_files:
|
|
|
|
file_path = os.path.join(lang_path, org_file)
|
|
|
|
try:
|
|
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
|
|
contenu = f.read()
|
|
|
|
# Compter les mots en divisant par les espaces
|
|
|
|
total_mots += len(contenu.split())
|
|
|
|
except Exception as e:
|
|
|
|
print(f"Erreur lors de la lecture de {file_path}: {e}")
|
|
|
|
|
|
|
|
# Ajouter ou incrémenter le compteur de mots dans les stats
|
|
|
|
stats[site]['nb_mots'] += total_mots
|
|
|
|
|
|
|
|
# Trouver le fichier le plus récent
|
|
|
|
for org_file in org_files:
|
|
|
|
file_path = os.path.join(lang_path, org_file)
|
|
|
|
mod_time = os.path.getmtime(file_path)
|
|
|
|
|
|
|
|
if (stats[site]['date_dernier_article'] is None or
|
|
|
|
mod_time > stats[site]['date_dernier_article']):
|
|
|
|
stats[site]['date_dernier_article'] = mod_time
|
|
|
|
stats[site]['dernier_article'] = file_path
|
|
|
|
|
|
|
|
# Convertir le timestamp en date lisible
|
|
|
|
if stats[site]['date_dernier_article']:
|
|
|
|
stats[site]['date_dernier_article'] = datetime.fromtimestamp(
|
|
|
|
stats[site]['date_dernier_article']
|
|
|
|
).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
|
|
return stats
|
2025-02-19 16:24:20 +01:00
|
|
|
|
|
|
|
def convert_org_to_html(org_file, output_html_file):
|
|
|
|
"""
|
|
|
|
Convertit un fichier Org en HTML en utilisant pypandoc.
|
|
|
|
|
|
|
|
:param org_file: Chemin du fichier Org à convertir.
|
|
|
|
:param output_html_file: Chemin du fichier HTML de sortie.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
pypandoc.convert_file(org_file, 'html', outputfile=output_html_file)
|
|
|
|
print(f"Conversion réussie : {org_file} -> {output_html_file}")
|
|
|
|
except Exception as e:
|
|
|
|
print(f"Erreur lors de la conversion de {org_file} : {e}")
|
2025-02-27 16:18:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_first_picture_url(content):
|
|
|
|
# Utiliser une expression régulière pour
|
|
|
|
# trouver la première URL d'image dans le contenu
|
|
|
|
pattern = r'\[\[(.*?)\]\]'
|
|
|
|
match = re.search(pattern, content)
|
|
|
|
if match:
|
|
|
|
return match.group(1)
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2025-02-27 19:34:49 +01:00
|
|
|
def org_to_gmi(org_text: str) -> str:
|
2025-02-27 16:18:47 +01:00
|
|
|
"""
|
|
|
|
Convertit un texte au format Org en un fichier au format GMI (Gemini)
|
|
|
|
en utilisant pypandoc.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
- org_text (str): Le texte au format Org à convertir.
|
|
|
|
"""
|
2025-02-27 19:34:49 +01:00
|
|
|
# Nettoyer le contenu org en retirant les lignes commençant par #+post
|
2025-02-28 19:48:30 +01:00
|
|
|
cleaned_content = '\n'.join(line for line in org_text.splitlines() if not line.strip().startswith('#+'))
|
2025-02-27 19:34:49 +01:00
|
|
|
org_content = cleaned_content
|
2025-02-28 19:48:30 +01:00
|
|
|
|
2025-02-27 19:34:49 +01:00
|
|
|
try:
|
|
|
|
converted_text = pypandoc.convert_text(org_content, 'markdown', format='org')
|
2025-02-28 19:48:30 +01:00
|
|
|
|
2025-02-28 19:25:09 +01:00
|
|
|
# Remplacer les apostrophes échappées
|
|
|
|
converted_text = converted_text.replace('\\\'', '\'')
|
|
|
|
|
2025-02-28 19:48:30 +01:00
|
|
|
# Convertir les liens simples <url>
|
|
|
|
converted_text = re.sub(r'<(https?://[^>]+)>', r'\n=> \1\n\n', converted_text)
|
|
|
|
|
|
|
|
|
|
|
|
# Nettoyer les espaces multiples et retours à la ligne dans les liens simples
|
|
|
|
converted_text = re.sub(r'(\=\> [^\n]+?)[ \n]{2,}', r'\1 ', converted_text)
|
|
|
|
|
|
|
|
# Nettoyer les espaces multiples et retours à la ligne dans les liens avec description
|
|
|
|
converted_text = re.sub(r'(\=\> [^\n]+?)[ \n]{2,}([^\n]+)', r'\1 \2', converted_text)
|
|
|
|
|
|
|
|
|
|
|
|
# Convertir les liens avec description [texte](url)
|
|
|
|
converted_text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\n=> \2 \1\n\n', converted_text)
|
|
|
|
|
|
|
|
# Ajouter un saut de ligne devant les lignes commençant par #
|
|
|
|
converted_text = re.sub(r'(?m)^#', r'\n#', converted_text)
|
|
|
|
|
|
|
|
# Remplacer les sauts de ligne multiples par un maximum de 2 sauts de ligne
|
|
|
|
converted_text = re.sub(r'\n{3,}', '\n\n', converted_text)
|
2025-02-28 19:25:09 +01:00
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
|
2025-02-27 19:34:49 +01:00
|
|
|
output = f"""
|
|
|
|
-------------------
|
2025-02-27 16:18:47 +01:00
|
|
|
|
2025-02-27 19:34:49 +01:00
|
|
|
{converted_text}
|
2025-02-27 16:18:47 +01:00
|
|
|
|
2025-02-27 19:34:49 +01:00
|
|
|
-------------------
|
2025-02-27 16:18:47 +01:00
|
|
|
"""
|
2025-02-27 19:34:49 +01:00
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
except RuntimeError as e:
|
|
|
|
print(f"Erreur de conversion : {e}")
|
|
|
|
return
|
2025-02-27 19:34:49 +01:00
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
return output
|
|
|
|
|
|
|
|
def count_files_in_directories(directories):
|
|
|
|
total_count = 0
|
2025-02-27 19:34:49 +01:00
|
|
|
# Exclure le dossier ".." du comptage
|
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
for directory in directories:
|
2025-02-27 19:34:49 +01:00
|
|
|
if directory == "..":
|
|
|
|
continue
|
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
for root, dirs, files in os.walk(directory):
|
2025-02-27 19:34:49 +01:00
|
|
|
|
|
|
|
print('files', files)
|
|
|
|
total_count += sum(1 for f in files if f.endswith(('.org', '.md', '.gmi')))
|
|
|
|
continue
|
2025-02-27 16:18:47 +01:00
|
|
|
return total_count
|
|
|
|
|
2025-02-28 00:40:13 +01:00
|
|
|
def format_date_str(date_str):
|
|
|
|
"""
|
|
|
|
Formate une chaîne de date dans différents formats possibles
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
# Définir la locale en français
|
|
|
|
try:
|
|
|
|
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
|
|
|
|
except:
|
|
|
|
try:
|
|
|
|
locale.setlocale(locale.LC_TIME, 'fra')
|
|
|
|
except:
|
|
|
|
print("Impossible de définir la locale en français")
|
|
|
|
|
|
|
|
# Format YYYYMMDDHHMMSS (14 caractères)
|
|
|
|
if len(date_str) == 14:
|
|
|
|
if 'T' not in date_str:
|
|
|
|
return datetime.strptime(date_str, '%Y%m%d%H%M%S').strftime('%d %B %Y à %H:%M:%S')
|
|
|
|
else:
|
|
|
|
# Si contient T, traiter comme YYYYMMDDTHHMM + 1 chiffre
|
|
|
|
base_date = date_str[:12]
|
|
|
|
return datetime.strptime(base_date, '%Y%m%dT%H%M').strftime('%d %B %Y à %H:%M')
|
|
|
|
|
|
|
|
# Format YYYYMMDDTHHMMSS (15 caractères avec T)
|
|
|
|
elif len(date_str) == 15 and 'T' in date_str:
|
|
|
|
return datetime.strptime(date_str, '%Y%m%dT%H%M%S').strftime('%d %B %Y à %H:%M:%S')
|
|
|
|
|
|
|
|
# Format YYYYMMDDTHHMM + chiffre optionnel (13 caractères avec T)
|
|
|
|
elif len(date_str) == 13 and 'T' in date_str:
|
|
|
|
# Toujours prendre les 12 premiers caractères (YYYYMMDDTHHMM)
|
|
|
|
base_date = date_str[:12]
|
|
|
|
return datetime.strptime(base_date, '%Y%m%dT%H%M').strftime('%d %B %Y à %H:%M')
|
|
|
|
|
|
|
|
# Format YYYY-MM-DD
|
|
|
|
elif len(date_str) == 10 and '-' in date_str:
|
|
|
|
return datetime.strptime(date_str, '%Y-%m-%d').strftime('%d %B %Y')
|
|
|
|
|
|
|
|
else:
|
|
|
|
print(f"Format de date non reconnu: {date_str}")
|
|
|
|
# Essayer d'extraire au moins la date de base
|
|
|
|
try:
|
|
|
|
base_date = date_str[:8] # Prendre juste YYYYMMDD
|
|
|
|
return datetime.strptime(base_date, '%Y%m%d').strftime('%d %B %Y')
|
|
|
|
except ValueError:
|
|
|
|
return date_str
|
|
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
print(f"Erreur lors du formatage de la date {date_str}: {str(e)}")
|
|
|
|
# En cas d'erreur, essayer de parser juste la partie date
|
|
|
|
try:
|
|
|
|
base_date = date_str[:8] # Prendre juste YYYYMMDD
|
|
|
|
return datetime.strptime(base_date, '%Y%m%d').strftime('%d %B %Y')
|
|
|
|
except ValueError:
|
|
|
|
return date_str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
|
|
|
|
def convert_org_to_gemini(org_content):
|
|
|
|
"""
|
|
|
|
Convertit un contenu org en gemini en utilisant pandoc et md2gemini
|
|
|
|
|
|
|
|
Args:
|
|
|
|
org_content (str): Contenu au format org
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
str: Contenu converti en format gemini
|
|
|
|
"""
|
2025-02-27 19:34:49 +01:00
|
|
|
|
|
|
|
# Nettoyer le contenu org en retirant les lignes commençant par #+post
|
|
|
|
cleaned_content = '\n'.join( line for line in org_content.splitlines() if not line.strip().startswith('#+') )
|
|
|
|
org_content = cleaned_content
|
|
|
|
|
2025-02-27 16:18:47 +01:00
|
|
|
try:
|
|
|
|
# Créer un fichier temporaire avec le contenu org
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.org', encoding='utf-8') as temp_org:
|
|
|
|
temp_org.write(org_content)
|
|
|
|
temp_org.flush()
|
|
|
|
|
|
|
|
# Première étape : conversion org vers markdown avec pandoc
|
|
|
|
pandoc_cmd = [
|
|
|
|
'pandoc',
|
|
|
|
'-f', 'org',
|
|
|
|
'-t', 'markdown',
|
|
|
|
temp_org.name
|
|
|
|
]
|
|
|
|
|
|
|
|
markdown_content = subprocess.check_output(
|
|
|
|
pandoc_cmd,
|
|
|
|
text=True,
|
|
|
|
stderr=subprocess.PIPE
|
|
|
|
)
|
|
|
|
|
|
|
|
# Deuxième étape : conversion markdown vers gemini avec md2gemini
|
|
|
|
gemini_content = md2gemini(
|
|
|
|
markdown_content,
|
|
|
|
frontmatter=True,
|
|
|
|
links='inline',
|
|
|
|
)
|
|
|
|
|
|
|
|
return gemini_content.strip()
|
|
|
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
print(f"Erreur lors de la conversion avec pandoc: {e.stderr}")
|
|
|
|
raise
|
|
|
|
except Exception as e:
|
|
|
|
print(f"Erreur lors de la conversion: {str(e)}")
|
|
|
|
raise
|
2025-02-27 19:34:49 +01:00
|
|
|
|
|
|
|
|
2025-02-28 00:26:19 +01:00
|
|
|
def save_gemini_file(blog_name, article, articles_info, template_content):
|
|
|
|
"""
|
|
|
|
Sauvegarde le contenu gemini d'un article
|
|
|
|
|
|
|
|
Args:
|
|
|
|
blog_name (str): Nom du blog
|
|
|
|
article (dict): Dictionnaire contenant les informations de l'article
|
|
|
|
articles_info (dict): Dictionnaire contenant tous les articles
|
|
|
|
template_content (str): Contenu du template
|
|
|
|
"""
|
|
|
|
# Créer le dossier de destination s'il n'existe pas
|
|
|
|
os.makedirs(f"gemini-capsules/{blog_name}/{article['annee']}", exist_ok=True)
|
2025-02-27 19:34:49 +01:00
|
|
|
|
|
|
|
|
2025-02-28 00:26:19 +01:00
|
|
|
# Construire les liens précédent/suivant
|
|
|
|
previous_article_link = ""
|
|
|
|
next_article_link = ""
|
|
|
|
tags = ""
|
|
|
|
if article['tags']:
|
|
|
|
tags = "Tags: " + ', '.join(article['tags'])
|
|
|
|
|
|
|
|
if article['previous'] and articles_info and article['previous'] in articles_info:
|
|
|
|
prev = articles_info[article['previous']]
|
|
|
|
print('prev', prev['title'])
|
|
|
|
slug = slugify_title(prev['title'])
|
|
|
|
previous_article_link = f"=> {prev['annee']}/{slug}.gmi {prev['date_formattee']} - {prev['title']}"
|
2025-02-27 19:34:49 +01:00
|
|
|
|
2025-02-28 00:26:19 +01:00
|
|
|
if article['next'] and articles_info and article['next'] in articles_info:
|
|
|
|
next_art = articles_info[article['next']]
|
|
|
|
print('next', next_art['title'])
|
|
|
|
slug = slugify_title(next_art['title'])
|
|
|
|
next_article_link = f"=> {next_art['annee']}/{slug}.gmi {next_art['date_formattee']} - {next_art['title']}"
|
2025-02-27 19:34:49 +01:00
|
|
|
|
2025-02-28 00:26:19 +01:00
|
|
|
# Construire le contenu gemini complet
|
|
|
|
gemini_content = f"""# {article['title']}
|
2025-02-27 19:34:49 +01:00
|
|
|
|
2025-02-28 00:26:19 +01:00
|
|
|
Date: {article['date_formattee']}
|
|
|
|
|
|
|
|
{article['gemini_content']}
|
|
|
|
-----------------------------------------------
|
2025-02-28 00:40:13 +01:00
|
|
|
|
2025-02-28 00:26:19 +01:00
|
|
|
{tags}
|
|
|
|
|
|
|
|
-----------------------------------------------
|
2025-02-28 00:40:13 +01:00
|
|
|
{template_content['AUTHOR']}
|
2025-02-28 00:26:19 +01:00
|
|
|
{template_content['SOUTIEN']}
|
|
|
|
-----------------------------------------------
|
|
|
|
Navigation:
|
|
|
|
"""
|
|
|
|
|
|
|
|
if previous_article_link:
|
|
|
|
gemini_content += f"\nArticle précédent:\n{previous_article_link}"
|
|
|
|
|
|
|
|
if next_article_link:
|
|
|
|
gemini_content += f"\nArticle suivant:\n{next_article_link}"
|
|
|
|
|
2025-02-28 19:48:30 +01:00
|
|
|
gemini_content += f"""
|
|
|
|
-----------------------------------------------
|
2025-02-28 00:26:19 +01:00
|
|
|
=> index.gmi Index des articles
|
|
|
|
"""
|
|
|
|
# Sauvegarder le fichier
|
|
|
|
slug = slugify_title(article['title'])
|
|
|
|
os.makedirs(f"gemini-capsules/{blog_name}/{article['annee']}", exist_ok=True)
|
|
|
|
with open(f"gemini-capsules/{blog_name}/{article['annee']}/{slug}.gmi", "w", encoding="utf-8") as f:
|
|
|
|
f.write(gemini_content)
|
2025-05-13 11:29:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
def get_series_name(article_slug, blog_name):
|
|
|
|
"""
|
|
|
|
Retourne le nom de la série d'un article s'il en fait partie.
|
|
|
|
|
|
|
|
:param article: Dictionnaire contenant les informations de l'article
|
|
|
|
:return: Le nom de la série ou None si l'article n'appartient à aucune série
|
|
|
|
"""
|
|
|
|
if blog_name in website_config:
|
|
|
|
for s in website_config[blog_name]['SERIES']:
|
|
|
|
if article_slug in website_config['SERIES'][blog_name][s]['articles']:
|
|
|
|
return s
|
|
|
|
return None
|