- Implemented `report.nim` to create structured reports on metadata modifications. - Added functionality to merge reports and convert them to formatted strings. docs: Create prompt documentation for Markdown parser project - Added `prompt.md` detailing the requirements and functionalities for the Markdown parser. - Included specifications, usage examples, and testing guidelines. docs: Generate code review report for Markdown parser - Created `rapport_revue_code.md` outlining security vulnerabilities, code quality issues, and suggested improvements. - Provided a detailed analysis of the codebase with actionable recommendations. test: Add test data for Markdown parser - Included various Markdown files and a JPG image in `test_data` to simulate different scenarios. - Ensured that the parser can handle both valid and invalid metadata. chore: Add version management file - Created `version.nim` for automatic versioning of the Markdown parser. - Established constants for major, minor, patch, and build versions.
404 lines
14 KiB
Nim
404 lines
14 KiB
Nim
# markdown_parser.nim
|
|
# Programme principal qui définit l'interface en ligne de commande et orchestre le processus
|
|
|
|
import
|
|
std/[strutils, os, times, strformat,parseopt, options],
|
|
modules/[fileutils, metadata, config, report],
|
|
version
|
|
|
|
# Initialiser le mode debug global à partir du fichier de configuration
|
|
let appConfigGlobal = loadConfig("config.json")
|
|
setGlobalDebugMode(appConfigGlobal.debugModeActive)
|
|
|
|
const
|
|
HELP_TEXT = """
|
|
markdown_parser - Analyseur et restructureur de fichiers Markdown
|
|
|
|
Usage:
|
|
markdown_parser [options]
|
|
|
|
Options:
|
|
-h, --help Affiche ce message d'aide
|
|
-v, --version Affiche la version du programme
|
|
-V, --verbose Mode verbeux pour afficher plus d'informations
|
|
-s, --source=PATH Chemin vers le répertoire source
|
|
-f, --files=FILE1,FILE2,... Liste de fichiers Markdown (séparés par des virgules)
|
|
-a, --analyze Active l'analyse IA des fichiers sans métadonnées
|
|
"""
|
|
|
|
type
|
|
AppConfig* = object
|
|
sourceDir*: string
|
|
files*: seq[string]
|
|
verbose*: bool
|
|
showHelp*: bool
|
|
showVersion*: bool
|
|
analyze*: bool
|
|
|
|
proc parseCommandLine(): AppConfig =
|
|
var config = AppConfig()
|
|
for kind, key, val in getopt():
|
|
case kind
|
|
of cmdLongOption, cmdShortOption:
|
|
case key
|
|
of "h", "help": config.showHelp = true
|
|
of "v", "version": config.showVersion = true
|
|
of "V", "verbose": config.verbose = true
|
|
of "s", "source": config.sourceDir = val
|
|
of "f", "files": config.files = val.split(',')
|
|
of "a", "analyze": config.analyze = true
|
|
else: discard
|
|
return config
|
|
|
|
proc backupAndRenameSourceDir(sourceDir: string, verbose: bool = false): string =
|
|
## Crée une sauvegarde du répertoire source, renomme le répertoire source
|
|
## avec un timestamp, et crée un nouveau répertoire avec le nom original.
|
|
##
|
|
## Paramètres:
|
|
## sourceDir: Chemin du répertoire source
|
|
## verbose: Active les messages de débogage détaillés
|
|
##
|
|
## Retourne:
|
|
## Le nom du répertoire source renommé en cas de succès, ou une chaîne vide en cas d'erreur
|
|
|
|
# Format timestamp for filenames - manually create timestamp string
|
|
# Get current time components
|
|
let now = now()
|
|
let year = $now.year
|
|
let month = align($(ord(now.month)), 2, '0') # Pad with leading zeros
|
|
let day = align($now.monthday, 2, '0') # Pad with leading zeros
|
|
let hour = align($now.hour, 2, '0') # Pad with leading zeros
|
|
let minute = align($now.minute, 2, '0') # Pad with leading zeros
|
|
let second = align($now.second, 2, '0') # Pad with leading zeros
|
|
|
|
# Concatenate timestamp components manually
|
|
let timestamp = year & month & day & hour & minute & second
|
|
|
|
# Create filenames
|
|
let backupFileName = fmt"{sourceDir}_{timestamp}.zip"
|
|
let newSourceDirName = fmt"{sourceDir}_{timestamp}"
|
|
|
|
# Create backup using zipDir function from fileutils module
|
|
let backupSuccess = zipDir(sourceDir, backupFileName, verbose)
|
|
|
|
if not backupSuccess:
|
|
echo fmt"Erreur: Échec de la création du backup {backupFileName}"
|
|
return ""
|
|
|
|
# Rename source directory
|
|
if verbose:
|
|
echo fmt"Renommage du répertoire source: {sourceDir} -> {newSourceDirName}"
|
|
|
|
try:
|
|
moveDir(sourceDir, newSourceDirName)
|
|
except OSError:
|
|
echo fmt"Erreur lors du renommage du répertoire source: {getCurrentExceptionMsg()}"
|
|
return ""
|
|
|
|
# Create new source directory with original name
|
|
try:
|
|
createDir(sourceDir)
|
|
if verbose:
|
|
echo fmt"Création du nouveau répertoire source: {sourceDir}"
|
|
except OSError:
|
|
echo fmt"Erreur lors de la création du nouveau répertoire: {getCurrentExceptionMsg()}"
|
|
return ""
|
|
|
|
return newSourceDirName
|
|
|
|
proc validateConfig(config: AppConfig): bool =
|
|
if config.showHelp or config.showVersion:
|
|
return true
|
|
|
|
if config.sourceDir == "" and config.files.len == 0:
|
|
echo "Erreur: Ni répertoire source ni fichiers spécifiés"
|
|
return false
|
|
|
|
if config.sourceDir != "" and not dirExists(config.sourceDir):
|
|
echo fmt"Erreur: Le répertoire source '{config.sourceDir}' n'existe pas"
|
|
return false
|
|
|
|
for file in config.files:
|
|
if not fileExists(file):
|
|
echo fmt"Erreur: Le fichier '{file}' n'existe pas"
|
|
return false
|
|
|
|
# Vérifier la disponibilité du modèle AI si l'analyse est activée
|
|
if config.analyze:
|
|
let appConfig = loadConfig("config.json")
|
|
if not isModelAvailable(appConfig):
|
|
echo fmt"Erreur: Le modèle AI '{appConfig.activeModelName}' n'est pas disponible. Analyse désactivée."
|
|
echo "Aucun traitement ne sera effectué."
|
|
return false
|
|
|
|
return true
|
|
|
|
proc processMarkdownFile(sourcePath, targetPath: string, verbose: bool, analyze: bool): FileReport =
|
|
## Traite un fichier Markdown en extrayant/générant les métadonnées
|
|
var fileReport = FileReport(
|
|
sourcePath: sourcePath,
|
|
targetPath: targetPath,
|
|
status: "",
|
|
success: false
|
|
)
|
|
|
|
try:
|
|
# Lire le contenu du fichier
|
|
let content = readFile(sourcePath)
|
|
var processedContent = content
|
|
let appConfig = loadConfig("config.json")
|
|
|
|
# Extraire les métadonnées existantes
|
|
if verbose:
|
|
echo fmt"Analyse du fichier: {sourcePath}"
|
|
|
|
var metadata = extractMetadataFromYaml(content)
|
|
|
|
# Si pas de métadonnées et analyse activée, utiliser l'IA
|
|
if metadata.isNone() and analyze:
|
|
if verbose:
|
|
echo "Pas de métadonnées trouvées, tentative d'analyse IA..."
|
|
|
|
let analysisResult = analyzeWithAI(getContentWithoutYaml(content), "config.json")
|
|
metadata = some(Metadata(
|
|
title: analysisResult.title,
|
|
description: analysisResult.description,
|
|
tags: analysisResult.tags,
|
|
creationDate: now().format("yyyy-MM-dd"),
|
|
category: extractCategoryFromPath(sourcePath)
|
|
))
|
|
processedContent = addMetadataToContent(metadata.get(), content)
|
|
|
|
if verbose:
|
|
let tagsJoined = analysisResult.tags.join(", ")
|
|
echo fmt"Métadonnées générées par l'IA: titre='{analysisResult.title}', description='{analysisResult.description}', tags=[{tagsJoined}]"
|
|
|
|
fileReport.status = "Métadonnées générées par IA"
|
|
fileReport.success = true
|
|
|
|
elif metadata.isSome():
|
|
if verbose:
|
|
echo "Métadonnées existantes trouvées"
|
|
fileReport.status = "Métadonnées existantes"
|
|
fileReport.success = true
|
|
else:
|
|
if verbose:
|
|
echo "Aucune métadonnée trouvée et analyse IA désactivée"
|
|
fileReport.status = "Aucune métadonnée"
|
|
fileReport.success = true
|
|
|
|
# Écrire le contenu traité dans le fichier cible
|
|
writeFile(targetPath, processedContent)
|
|
|
|
except Exception as e:
|
|
echo fmt"Erreur lors du traitement du fichier {sourcePath}: {e.msg}"
|
|
fileReport.status = fmt"Erreur: {e.msg}"
|
|
fileReport.success = false
|
|
|
|
return fileReport
|
|
|
|
proc processMarkdownFiles(config: AppConfig): Report =
|
|
var report = newReport()
|
|
var newSourceDir = ""
|
|
|
|
# Vérifier la disponibilité du modèle AI si l'analyse est activée
|
|
if config.analyze:
|
|
let appConfig = loadConfig("config.json")
|
|
if not isModelAvailable(appConfig):
|
|
echo fmt"Erreur: Le modèle AI '{appConfig.activeModelName}' n'est pas disponible. Analyse désactivée."
|
|
echo "Aucun traitement ne sera effectué."
|
|
return report
|
|
|
|
# Si un répertoire source est spécifié, traiter tous les fichiers
|
|
if config.sourceDir != "":
|
|
if config.verbose:
|
|
echo fmt"Analyse du répertoire source: {config.sourceDir}"
|
|
|
|
newSourceDir = backupAndRenameSourceDir(config.sourceDir, config.verbose)
|
|
if newSourceDir == "":
|
|
echo "Erreur: Impossible de préparer les répertoires"
|
|
return report
|
|
|
|
# Récupérer récursivement tous les fichiers du répertoire source
|
|
let allFiles = walkDirRecFiles(newSourceDir, "")
|
|
if config.verbose:
|
|
echo fmt"Nombre total de fichiers trouvés: {allFiles.len}"
|
|
|
|
for file in allFiles:
|
|
let
|
|
relPath = relativePath(file, newSourceDir)
|
|
targetFile = config.sourceDir / relPath
|
|
parentDir = parentDir(targetFile)
|
|
|
|
if not dirExists(parentDir):
|
|
createDir(parentDir)
|
|
|
|
# Vérifier si c'est un fichier markdown
|
|
if file.endsWith(".md"):
|
|
# Traiter les fichiers markdown avec processMarkdownFile
|
|
let fileReport = processMarkdownFile(file, targetFile, config.verbose, config.analyze)
|
|
report.fileReports.add(fileReport)
|
|
else:
|
|
# Copier simplement les autres fichiers
|
|
if config.verbose:
|
|
echo fmt"Copie du fichier non-markdown: {file} -> {targetFile}"
|
|
discard copyFileWithDirectories(file, targetFile, config.verbose)
|
|
|
|
# Après le traitement, supprimer le répertoire temporaire
|
|
try:
|
|
if config.verbose:
|
|
echo fmt"Suppression du répertoire temporaire: {newSourceDir}"
|
|
removeDir(newSourceDir)
|
|
except Exception as e:
|
|
echo fmt"Erreur lors de la suppression du répertoire temporaire: {e.msg}"
|
|
|
|
# Traiter les fichiers spécifiés individuellement avec l'option --files
|
|
elif config.files.len > 0:
|
|
# Créer un répertoire temporaire pour les fichiers d'origine
|
|
let
|
|
now = now()
|
|
year = $now.year
|
|
month = align($(ord(now.month)), 2, '0')
|
|
day = align($now.monthday, 2, '0')
|
|
hour = align($now.hour, 2, '0')
|
|
minute = align($now.minute, 2, '0')
|
|
second = align($now.second, 2, '0')
|
|
timestamp = year & month & day & hour & minute & second
|
|
tempDir = "markdown_files_" & timestamp
|
|
|
|
# Créer le répertoire temporaire
|
|
createDir(tempDir)
|
|
if config.verbose:
|
|
echo fmt"Création du répertoire temporaire: {tempDir}"
|
|
|
|
# Déterminer le répertoire cible (utiliser sourceDir s'il est spécifié, sinon "processed_files")
|
|
var targetBaseDir = if config.sourceDir != "": config.sourceDir else: "processed_files"
|
|
|
|
# Normaliser le chemin cible
|
|
targetBaseDir = normalizedPath(targetBaseDir)
|
|
|
|
# Créer le répertoire cible s'il n'existe pas
|
|
if not dirExists(targetBaseDir):
|
|
createDir(targetBaseDir)
|
|
if config.verbose:
|
|
echo fmt"Création du répertoire cible: {targetBaseDir}"
|
|
|
|
# Créer une sauvegarde du répertoire cible s'il contient déjà des fichiers
|
|
if dirExists(targetBaseDir) and walkDirRecFiles(targetBaseDir, "").len > 0:
|
|
let backupSuccess = zipDir(targetBaseDir, "", config.verbose)
|
|
if not backupSuccess and config.verbose:
|
|
echo fmt"Avertissement: Échec de la création du backup pour {targetBaseDir}"
|
|
|
|
var processedPaths: seq[string] = @[]
|
|
|
|
# Traiter chaque fichier spécifié
|
|
for file in config.files:
|
|
let
|
|
absFilePath = absolutePath(file)
|
|
fileDir = parentDir(absFilePath)
|
|
|
|
# Déterminer le chemin relatif en préservant la structure du dossier original
|
|
var relPath: string
|
|
|
|
# Si un répertoire source est spécifié et que le fichier est à l'intérieur
|
|
if config.sourceDir != "" and absFilePath.startsWith(absolutePath(config.sourceDir)):
|
|
relPath = relativePath(absFilePath, absolutePath(config.sourceDir))
|
|
else:
|
|
# Si aucun répertoire source n'est spécifié, préserver la structure à partir du dossier parent du fichier
|
|
let filename = extractFilename(absFilePath)
|
|
# Utiliser seulement le dernier dossier parent comme sous-répertoire
|
|
let parentDirName = extractFilename(fileDir)
|
|
if parentDirName != "":
|
|
relPath = parentDirName / filename
|
|
else:
|
|
relPath = filename
|
|
|
|
# Chemins pour fichiers temporaires et cibles
|
|
let
|
|
tempFilePath = tempDir / relPath
|
|
targetFile = targetBaseDir / relPath
|
|
targetDir = parentDir(targetFile)
|
|
|
|
# Ajouter à la liste des chemins traités
|
|
processedPaths.add(targetFile)
|
|
|
|
# Créer les répertoires parents nécessaires pour le fichier temporaire
|
|
let tempFileDir = parentDir(tempFilePath)
|
|
if not dirExists(tempFileDir):
|
|
createDir(tempFileDir)
|
|
|
|
# Créer le répertoire parent cible s'il n'existe pas
|
|
if not dirExists(targetDir):
|
|
createDir(targetDir)
|
|
if config.verbose:
|
|
echo fmt"Création du répertoire parent cible: {targetDir}"
|
|
|
|
# Copier le fichier dans le répertoire temporaire
|
|
if config.verbose:
|
|
echo fmt"Copie du fichier original vers le répertoire temporaire: {absFilePath} -> {tempFilePath}"
|
|
discard copyFileWithDirectories(absFilePath, tempFilePath, config.verbose)
|
|
|
|
# Traiter le fichier markdown
|
|
if file.endsWith(".md"):
|
|
if config.verbose:
|
|
echo fmt"Traitement du fichier markdown: {tempFilePath} -> {targetFile}"
|
|
let fileReport = processMarkdownFile(tempFilePath, targetFile, config.verbose, config.analyze)
|
|
report.fileReports.add(fileReport)
|
|
else:
|
|
# Copier simplement les fichiers non-markdown
|
|
if config.verbose:
|
|
echo fmt"Copie du fichier non-markdown: {tempFilePath} -> {targetFile}"
|
|
discard copyFileWithDirectories(tempFilePath, targetFile, config.verbose)
|
|
|
|
# Créer une sauvegarde du répertoire temporaire
|
|
let backupDir = "backup"
|
|
if not dirExists(backupDir):
|
|
createDir(backupDir)
|
|
|
|
let tempBackupSuccess = zipDir(tempDir, backupDir / (tempDir & ".zip"), config.verbose)
|
|
if tempBackupSuccess:
|
|
if config.verbose:
|
|
echo fmt"Sauvegarde du répertoire temporaire créée: {backupDir}/{tempDir}.zip"
|
|
else:
|
|
echo fmt"Erreur lors de la création de la sauvegarde du répertoire temporaire: {tempDir}"
|
|
|
|
# Supprimer le répertoire temporaire après la sauvegarde
|
|
try:
|
|
if config.verbose:
|
|
echo fmt"Suppression du répertoire temporaire: {tempDir}"
|
|
removeDir(tempDir)
|
|
except Exception as e:
|
|
echo fmt"Erreur lors de la suppression du répertoire temporaire: {e.msg}"
|
|
|
|
return report
|
|
|
|
proc main() =
|
|
let config = parseCommandLine()
|
|
|
|
if config.showHelp:
|
|
echo HELP_TEXT
|
|
quit(0)
|
|
|
|
if config.showVersion:
|
|
echo fmt"markdown_parser version {VERSION_STRING}"
|
|
quit(0)
|
|
|
|
if not validateConfig(config):
|
|
echo HELP_TEXT
|
|
quit(1)
|
|
|
|
if config.verbose:
|
|
echo "Configuration validée, début du traitement..."
|
|
|
|
let report = processMarkdownFiles(config)
|
|
|
|
echo "\nRapport de traitement:"
|
|
let reportStr = report.toString()
|
|
echo reportStr.replace("\\n", "\n")
|
|
|
|
if config.verbose:
|
|
echo "Traitement terminé."
|
|
|
|
when isMainModule:
|
|
main()
|