markdown_parser/markdown_parser.nim
Bruno Charest 18ee8a1cfd feat: Add report generation module for tracking metadata changes
- 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.
2026-04-19 12:56:55 -04:00

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()