# metadata.nim # Module responsable de l'extraction et de la manipulation des métadonnées YAML import std/[strutils, strformat, options, times, os, httpclient, json, sequtils, tables, uri] import config # Fonctions utilitaires proc isEmptyOrWhitespace*(s: string): bool = ## Vérifie si une chaîne est vide ou contient uniquement des espaces blancs if s.len == 0: return true for c in s: if not c.isSpaceAscii(): return false return true proc parseYamlValue(line: string): string = ## Extrait la valeur d'une ligne YAML au format "clé: valeur" let parts = line.split(':', 1) if parts.len > 1: return parts[1].strip() return "" proc isValidTag*(tag: string): bool = ## Vérifie si un tag est valide (contient au moins un caractère alphanumérique) ## Retourne false pour les tags qui ne contiennent que des caractères spéciaux comme "--" if tag.len == 0: return false # Vérifier si le tag contient au moins une lettre ou un chiffre for c in tag: if c.isAlphaNumeric(): return true return false # Tag ne contient que des caractères spéciaux proc cleanTagText*(tag: string): string = ## Nettoie un tag en remplaçant les espaces par des underscores et en éliminant les caractères indésirables var resultString = tag.strip() # Supprimer les guillemets s'ils encadrent le tag if resultString.len >= 2 and resultString[0] == '"' and resultString[^1] == '"': resultString = resultString[1..^2] # Supprimer les apostrophes s'ils encadrent le tag if resultString.len >= 2 and resultString[0] == '\'' and resultString[^1] == '\'': resultString = resultString[1..^2] # Convertir en minuscules resultString = resultString.toLowerAscii() # Remplacer les espaces par des underscores resultString = resultString.replace(" ", "_") # Remplacer certains caractères spéciaux par des underscores for c in ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '=', '{', '}', '[', ']', '|', '\\', ':', ';', '"', '\'', '<', '>', ',', '.', '?', '/']: resultString = resultString.replace($c, "_") # Limiter les underscores consécutifs à un seul while "__" in resultString: resultString = resultString.replace("__", "_") # Supprimer les underscores au début et à la fin resultString = resultString.strip(chars={'_'}) return resultString proc parseYamlSeq(lines: seq[string], startIndex: int): tuple[tags: seq[string], endIndex: int] = ## Parse une séquence YAML commençant par des tirets var tags: seq[string] = @[] i = startIndex while i < lines.len: let line = lines[i].strip() if line.startsWith("-"): let tagText = line[1..^1].strip() let cleanedTag = cleanTagText(tagText) if isValidTag(cleanedTag): # Ne garder que les tags valides tags.add(cleanedTag) i += 1 else: break return (tags: tags, endIndex: i) type AIAnalysisResult* = object title*: string description*: string tags*: seq[string] Metadata* = object title*: string description*: string tags*: seq[string] creationDate*: string creationTime*: string modificationDate*: string modificationTime*: string auteur*: string url*: string lang*: string category*: string isNew*: bool # Indique si les métadonnées ont été générées ou étaient déjà présentes proc newMetadata*(): Metadata = ## Crée un nouvel objet Metadata vide result = Metadata( title: "", description: "", tags: @[], creationDate: "", creationTime: "", modificationDate: "", modificationTime: "", auteur: "Non spécifié", url: "", lang: "fr", # Langue par défaut category: "", isNew: true ) proc formatCurrentDateTime*(): tuple[date: string, time: string] = ## Retourne la date et l'heure actuelles formatées selon le format requis let now = now() return ( date: now.format("yyyy-MM-dd"), time: now.format("HH:mm:ss") ) proc formatFileTime*(time: Time): tuple[date: string, time: string] = ## Formate un objet Time en tuple (date, heure) selon le format requis return ( date: time.format("yyyy-MM-dd"), time: time.format("HH:mm:ss") ) proc getFileTimeInfo*(filePath: string): tuple[creation: tuple[date: string, time: string], modification: tuple[date: string, time: string]] = ## Obtient les informations de date/heure de création et modification du fichier try: let fileInfo = getFileInfo(filePath) creationTime = fileInfo.creationTime modificationTime = fileInfo.lastWriteTime return ( creation: formatFileTime(creationTime), modification: formatFileTime(modificationTime) ) except: let current = formatCurrentDateTime() return ( creation: current, modification: current ) proc extractTitle*(content: string, filePath: string = ""): string = ## Tente d'extraire un titre à partir du contenu Markdown ## Regarde la première ligne commençant par # ou utilise le nom du fichier let lines = content.splitLines() for line in lines: let trimmed = line.strip() if trimmed.startsWith("# "): return trimmed[2..^1] # Si pas de titre trouvé, utiliser le nom du fichier sans extension if filePath != "": let fileName = extractFilename(filePath) if fileName.contains('.'): return fileName.split('.')[0] else: return fileName return "Document Markdown" proc extractYamlValue(yaml: string, key: string): string = ## Extrait la valeur d'une clé spécifique d'un bloc YAML let lines = yaml.splitLines() for line in lines: let trimmedLine = line.strip() # Cherche une ligne qui commence par la clé suivie de deux points if trimmedLine.startsWith(key & ":"): # Extrait la partie après les deux points let value = trimmedLine.substr(key.len + 1).strip() return value return "" proc extractYamlList(yaml: string, key: string): seq[string] = ## Extrait une liste de valeurs pour une clé spécifique d'un bloc YAML var resultList: seq[string] = @[] let lines = yaml.splitLines() var i = 0 # Chercher la ligne avec la clé while i < lines.len: if lines[i].strip().startsWith(key & ":"): # Si la clé est suivie directement de valeurs sur la même ligne let restOfLine = lines[i].substr(key.len + 1).strip() if restOfLine.len > 0 and not restOfLine.startsWith("-"): # Format compact: key: [val1, val2, val3] if restOfLine.startsWith("[") and restOfLine.endsWith("]"): let items = restOfLine[1..^2].split(",") for item in items: let cleaned = item.strip() if cleaned.len > 0: resultList.add(cleaned) break # Sinon, chercher les éléments de liste sur les lignes suivantes i += 1 while i < lines.len: let line = lines[i].strip() if line.startsWith("-"): let value = line[1..^1].strip() if value.len > 0: resultList.add(value) else: if not line.isEmptyOrWhitespace(): break i += 1 break i += 1 return resultList proc extractYamlBlock*(content: string): Option[string] = ## Extrait un bloc YAML d'une chaîne de caractères, délimité par des marqueurs "---" let lines = content.splitLines() # Vérifier si le contenu commence par un délimiteur YAML if lines.len < 3 or lines[0] != "---": return none(string) # Pas de section YAML var endYamlIndex = -1 # Chercher la fin de la section YAML for j in 1.. 0: # Extraction des valeurs title = extractYamlValue(yaml, "title") description = extractYamlValue(yaml, "description") tags = extractYamlList(yaml, "tags") if appConfig.debugModeActive: echo fmt"[DEBUG] Titre extrait: {title}" echo fmt"[DEBUG] Description extraite: {description}" echo fmt"[DEBUG] Tags extraits: {tags.len} tags" return AIAnalysisResult(title: title, description: description, tags: tags) else: echo fmt"Erreur lors de l'appel à l'API: {response.code} - {response.body}" return AIAnalysisResult(title: "", description: "", tags: @[]) except Exception as e: echo fmt"Exception lors de l'appel à l'API: {e.msg}" return AIAnalysisResult(title: "", description: "", tags: @[]) finally: client.close() proc extractCategoryFromPath*(filePath: string): string = ## Extrait une catégorie basée sur le chemin du fichier ## Format: Parent/Enfant/SousEnfant if filePath.len == 0: return "Non classé" # Normaliser le chemin en utilisant / plutôt que \ var normalizedPath = filePath.replace('\\', '/') # Obtenir le chemin relatif en supprimant les chemins absolus let driveLetterPos = normalizedPath.find(":/") pathStart = if driveLetterPos != -1: driveLetterPos + 2 else: 0 relativePath = normalizedPath[pathStart..^1] # Diviser le chemin en parties var parts = relativePath.split('/') # Retirer le nom du fichier (dernier élément) if parts.len > 0 and parts[^1].contains('.'): discard parts.pop() # Filtrer les parties vides et les noms de répertoires spéciaux parts = parts.filterIt(it.len > 0 and it != "." and it != "..") # Si après filtrage le chemin est vide, renvoyer catégorie par défaut if parts.len == 0: return "Non classé" # Rejoindre les parties avec un / result = parts.join("/") proc updateMetadata*(metadata: var Metadata, content: string, filePath: string, useAI: bool = true, forceUpdateCategory: bool = true): bool = ## Complète les métadonnées manquantes ## Retourne vrai si des modifications ont été apportées ## ## Paramètres: ## - metadata: Les métadonnées à mettre à jour ## - content: Le contenu du fichier Markdown ## - filePath: Chemin complet vers le fichier ## - useAI: Utiliser l'IA pour compléter les métadonnées manquantes ## - forceUpdateCategory: Si vrai, remplace toujours la catégorie par celle extraite du chemin var modified = false # Utiliser l'IA pour compléter les métadonnées manquantes if useAI: if metadata.title == "" or metadata.description == "" or metadata.tags.len == 0: # Analyser le contenu avec l'IA let aiResult = analyzeWithAI(content) # Compléter le titre s'il est manquant if metadata.title == "" and aiResult.title != "": metadata.title = aiResult.title modified = true # Compléter la description si elle est manquante if metadata.description == "" and aiResult.description != "": metadata.description = aiResult.description modified = true # Compléter les tags s'ils sont manquants if metadata.tags.len == 0 and aiResult.tags.len > 0: # Filtrer pour ne garder que les tags valides et nettoyer les espaces var cleanedTags: seq[string] = @[] for tag in aiResult.tags: let cleanedTag = cleanTagText(tag) if isValidTag(cleanedTag): cleanedTags.add(cleanedTag) metadata.tags = cleanedTags if metadata.tags.len > 0: modified = true # Méthodes traditionnelles si l'IA n'est pas utilisée ou n'a pas fourni de données if metadata.title == "": metadata.title = extractTitle(content, filePath) modified = true # Obtenir la date de création réelle du fichier let fileExists = fileExists(filePath) currentDateTime = formatCurrentDateTime() # Date/heure actuelle # Utiliser la date de création réelle du fichier si elle existe if metadata.creationDate == "" or metadata.creationTime == "": if fileExists: try: let creationTime = getCreationTime(filePath) let formattedCreationDate = format(creationTime, "yyyy-MM-dd") let formattedCreationTime = format(creationTime, "HH:mm:ss") if metadata.creationDate == "": metadata.creationDate = formattedCreationDate modified = true if metadata.creationTime == "": metadata.creationTime = formattedCreationTime modified = true except: # En cas d'erreur, utiliser la date/heure actuelle comme fallback if metadata.creationDate == "": metadata.creationDate = currentDateTime.date modified = true if metadata.creationTime == "": metadata.creationTime = currentDateTime.time modified = true else: # Pour les nouveaux fichiers, utiliser la date/heure actuelle if metadata.creationDate == "": metadata.creationDate = currentDateTime.date modified = true if metadata.creationTime == "": metadata.creationTime = currentDateTime.time modified = true if metadata.auteur == "": metadata.auteur = "Non spécifié" modified = true if metadata.lang == "": metadata.lang = "fr" modified = true if forceUpdateCategory or metadata.category == "" or metadata.category == "Non classé": # Utiliser la structure des répertoires comme catégorie let pathCategory = extractCategoryFromPath(filePath) echo fmt">>> extractCategoryFromPath pour {filePath} a retourné: {pathCategory}" # Toujours mettre à jour la catégorie si forceUpdateCategory est true echo fmt">>> Mise à jour de la catégorie: {metadata.category} -> {pathCategory}" metadata.category = pathCategory modified = true # Nettoyer les tags existants pour éliminer les tags non valides if metadata.tags.len > 0: let originalTags = metadata.tags # Garder une copie des tags originaux var cleanedTags: seq[string] = @[] for tag in metadata.tags: let cleanedTag = cleanTagText(tag) if isValidTag(cleanedTag): cleanedTags.add(cleanedTag) # Vérifier si les tags ont été modifiés (nombre différent ou contenu différent) if originalTags.len != cleanedTags.len or originalTags != cleanedTags: modified = true metadata.tags = cleanedTags # Si des modifications ont été apportées, mettre à jour la date et l'heure de modification # pour refléter le moment actuel du traitement if modified: metadata.modificationDate = currentDateTime.date metadata.modificationTime = currentDateTime.time else: # Si aucune modification n'a été apportée, vérifier si la date/heure de modification est définie # Si non, utiliser la date de dernière modification du fichier ou la date actuelle if metadata.modificationDate == "" or metadata.modificationTime == "": if fileExists: try: let modificationTime = getLastModificationTime(filePath) let formattedModDate = format(modificationTime, "yyyy-MM-dd") let formattedModTime = format(modificationTime, "HH:mm:ss") if metadata.modificationDate == "": metadata.modificationDate = formattedModDate modified = true # Marquer comme modifié même si on utilise juste la date du fichier if metadata.modificationTime == "": metadata.modificationTime = formattedModTime modified = true # Marquer comme modifié même si on utilise juste l'heure du fichier except: # En cas d'erreur, utiliser la date/heure actuelle if metadata.modificationDate == "": metadata.modificationDate = currentDateTime.date modified = true if metadata.modificationTime == "": metadata.modificationTime = currentDateTime.time modified = true else: # Pour les nouveaux fichiers, utiliser la date/heure actuelle if metadata.modificationDate == "": metadata.modificationDate = currentDateTime.date modified = true if metadata.modificationTime == "": metadata.modificationTime = currentDateTime.time modified = true return modified proc metadataToYaml*(metadata: Metadata): string = ## Convertit les métadonnées en YAML formaté # Créer les lignes YAML individuellement sans l'opérateur &= var lines: seq[string] = @["---"] lines.add(fmt"Titre: {metadata.title}") lines.add(fmt"Description: {metadata.description}") lines.add("tags:") for tag in metadata.tags: lines.add(fmt" - {tag}") lines.add(fmt"Date de création: {metadata.creationDate}") lines.add(fmt"Heure de création: {metadata.creationTime}") lines.add(fmt"Date de modification: {metadata.modificationDate}") lines.add(fmt"Heure de modification: {metadata.modificationTime}") lines.add(fmt"Auteur: {metadata.auteur}") lines.add(fmt"URL: {metadata.url}") lines.add(fmt"Lang: {metadata.lang}") lines.add(fmt"Catégorie: {metadata.category}") lines.add("---") # Joindre toutes les lignes avec des retours à la ligne return lines.join("\n") proc getContentWithoutYaml*(content: string): string = ## Retourne le contenu sans la section YAML initiale, s'il y en a une let lines = content.splitLines() if lines.len >= 2 and lines[0] == "---": # Chercher la ligne de fin de la section YAML for i in 1..