commit d216e81fb20f06ecd7805227665a3ccb55c1c08d Author: Axelle Abbadie Date: Tue May 12 01:55:27 2026 +0200 init diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..363eea7 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e4ad3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Notes de développement internes (Claude Code) +CLAUDE.md + +# Vault de test Obsidian — peut contenir des transcriptions avec données personnelles +test/ + +# Fichiers système +.DS_Store +Thumbs.db + +# Dépendances Node +node_modules/ + +# Sourcemaps (inutiles en production) +*.js.map + +# Données Obsidian locales du plugin (paramètres utilisateur) +data.json + +# Variables d'environnement locales +.env.local diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1f4c4bb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b2640bf --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +----------------------------------------------------------------------------- +Code source — The Beerware License (Revision 42) +----------------------------------------------------------------------------- + +Axelle Abbadie a écrit ce logiciel. +Vous pouvez faire ce que vous voulez avec, tant que vous conservez cette notice. +Si on se croise un jour et que vous pensez que ça valait le coup, +vous pouvez m'offrir une bière. + +----------------------------------------------------------------------------- +Documentation (SPECS.md, ROADMAP.md, README.md) — CC BY 4.0 +----------------------------------------------------------------------------- + +Creative Commons Attribution 4.0 International +https://creativecommons.org/licenses/by/4.0/ + +Vous êtes libres de partager et d'adapter ces documents, y compris à des fins +commerciales, à condition de créditer la source (Axelle Abbadie, PseudObsidian-ization). diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb34cfa --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +# PseudObsidian-ization + +**Plugin Obsidian de pseudonymisation et de correction de transcriptions d'entretiens et de corpus interactionnels.** + +Conçu pour les chercheurs en **analyse conversationnelle (EMCA)** et en **sciences du langage**, notamment à l'**UMR ICAR** (CNRS / Université Lyon 2 / ENS Lyon), ce plugin permet de travailler directement dans Obsidian sur des transcriptions brutes issues de terrains de recherche, en produisant des versions pseudonymisées exploitables pour l'analyse et l'archivage. + +> **English summary** — An Obsidian plugin for pseudonymizing and correcting interactional transcripts (Jefferson / ICOR conventions, SRT, CHAT/CHA formats). Designed for qualitative researchers in linguistics and conversation analysis. See [SPECS.md](SPECS.md) for full technical specifications. + +--- + +## Fonctionnalités + +### Formats pris en charge +- `.srt` — sous-titres horodatés (sortie Whisper / IA) +- `.cha` / `.chat` — format CHAT / CLAN (CLAN, corpus de linguistique) +- `.md` — transcription Markdown (conventions Jefferson ou ICOR) +- `.txt` — texte brut + +Les fichiers `.srt` et `.cha` sont **automatiquement convertis en Markdown** à l'import, pour une édition native dans Obsidian. La structure (horodatages, locuteurs, métadonnées `@`, lignes dépendantes `%`) est préservée. + +### Pseudonymisation +- **Sélection → clic droit → Pseudonymiser** : remplace immédiatement dans le fichier +- **Pseudonymiser avec Coulmont** : interroge l'outil de Baptiste Coulmont ([coulmont.com](https://coulmont.com)) pour proposer des prénoms sociologiquement équivalents (même milieu social, même décennie de popularité) — le chercheur choisit dans la liste proposée +- **Créer une règle** : ajoute une correspondance dans le mapping JSON sans modifier le texte, pour validation ultérieure +- **Scanner le fichier courant** : liste toutes les occurrences des règles avec diff avant/après, validation occurrence par occurrence +- **Portées** : fichier, dossier, vault entier + +### Tables de correspondance +- Format JSON structuré (voir `SPECS.md §5`) +- Trois niveaux : `fichier.mapping.json`, `dossier.mapping.json`, `vault.mapping.json` +- Statuts : `suggested`, `validated`, `ignored`, `partial`, `conflict`, `needs_review` +- Priorité z-index : entier libre, comme le `z-index` CSS — les entités longues priment sur les plus courtes par défaut (protection `Saint-Jean-de-Luz` vs `Jean`) +- Marqueur optionnel dans l'export : `⟦Pierre⟧` pour identifier visuellement les pseudonymes + +### Surlignage dans l'éditeur +- **Orange** : termes sources encore présents (à pseudonymiser) +- **Vert** : pseudonymes déjà appliqués + +### Sécurité +- Tout traitement est **local** — aucune donnée de transcription n'est envoyée à un serveur externe +- Exception documentée : "Pseudonymiser avec Coulmont" envoie le prénom à `coulmont.com` (pas le contenu de la transcription) +- Tables de correspondance strictement séparées des exports + +--- + +## Statut actuel + +**Version en développement actif — 0.0.1 (alpha)** + +| Phase | Statut | Description | +|---|---|---| +| 0 — Boilerplate | ✅ | Plugin Obsidian TypeScript, Jest, ESLint | +| 1 — Parser SRT | ✅ | Round-trip exact, horodatages préservés | +| 2 — Parser CHAT | ✅ | Lignes `@`, `*`, `%` préservées | +| 3 — Moteur de pseudonymisation | ✅ | Spans, z-index, protection des chevauchements | +| 4 — Commandes UI | ✅ | Import, création de règles, export avec marqueur | +| 5 — Portées + surlignage | ✅ | ScopeResolver, CM6 ViewPlugin | +| 6 — Validation sélective | ✅ | OccurrencesModal, statuts partiels | +| **7 — Dictionnaires** | 🔄 **En cours** | Intégration Coulmont opérationnelle — import de dictionnaires JSON/CSV et constitution de ressources lexicales embarquées en cours | +| 8 — Scan automatique | ⏳ | Détection sans règle préexistante | +| 9 — Interface complète | ⏳ | Panneau latéral 4 onglets | +| 10 — Publication | ⏳ | Communauté Obsidian | + +--- + +## Installation + +### Pour les utilisateurs + +1. Télécharger la dernière release (fichiers `main.js`, `manifest.json`, `styles.css`) +2. Copier ces trois fichiers dans `.obsidian/plugins/pseudobsidianization/` de votre vault +3. Activer le plugin dans Obsidian → Paramètres → Extensions communautaires + +> Le plugin n'est pas encore soumis au répertoire communautaire Obsidian. Publication prévue à partir de la version 0.1.0. + +### Pour les développeurs et contributeurs + +```bash +git clone https://gitlab.huma-num.fr/[votre-groupe]/pseudobsidianization.git +cd pseudobsidianization +npm install +npm run dev # build en mode watch +npm test # suite de tests Jest +npm run build # build de production +``` + +Copier `main.js`, `manifest.json` et `styles.css` dans `.obsidian/plugins/pseudobsidianization/` d'un vault Obsidian dédié au développement. +Installer [Hot Reload](https://github.com/pjeby/hot-reload) dans ce vault et créer un fichier `.hotreload` dans le dossier du plugin pour le rechargement automatique à chaque build. + +#### Structure du dépôt + +``` +pseudobsidianization/ +├── src/ +│ ├── main.ts # Point d'entrée Obsidian +│ ├── types.ts # Types partagés (SPECS §11.3) +│ ├── settings.ts # Paramètres +│ ├── parsers/ # SrtParser, ChatParser, TranscriptConverter +│ ├── mappings/ # MappingStore, ScopeResolver +│ ├── pseudonymizer/ # Moteur, ReplacementPlanner, SpanProtector +│ ├── scanner/ # OccurrenceScanner +│ └── ui/ # Modales, surlignage CM6 +├── tests/ # Tests unitaires Jest +├── manifest.json # Métadonnées du plugin Obsidian +├── versions.json +├── styles.css +├── SPECS.md # Spécifications fonctionnelles complètes +├── ROADMAP.md # Feuille de route détaillée +└── README.md +``` + +--- + +## Contribuer + +Les contributions sont les bienvenues, en particulier : + +- **Dictionnaires** : listes de prénoms, toponymes, institutions adaptés à des corpus spécifiques (langues régionales, périodes historiques, terrains non francophones) +- **Conventions de transcription** : parsers pour d'autres systèmes (ELAN, Praat TextGrid, EXMARaLDA) +- **Adaptateurs** : connecteurs vers d'autres ressources lexicales ouvertes +- **Retours d'usage** : issues GitLab pour signaler des cas limites rencontrés sur de vrais corpus + +Merci d'ouvrir une issue avant de proposer une pull request pour les fonctionnalités importantes. + +--- + +## Licence + +### Code — *The Beerware License* (Revision 42) + +``` +Axelle Abbadie a écrit ce logiciel. +Vous pouvez faire ce que vous voulez avec, tant que vous conservez cette notice. +Si on se croise un jour et que vous pensez que ça valait le coup, +vous pouvez m'offrir une bière. +``` + +**Ce plugin est fait pour être modifié.** Si votre terrain de recherche implique des conventions de transcription particulières, un dialecte régional, des corpus multilingues ou des formats d'export spécifiques à votre institution, ne vous gênez pas pour adapter le code à vos besoins. C'est précisément l'esprit de cet outil. + +### Documentation et spécifications — CC BY 4.0 + +Les fichiers `SPECS.md`, `ROADMAP.md` et ce `README.md` sont placés sous licence [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/). + +Vous êtes libres de les réutiliser, adapter et redistribuer, y compris à des fins commerciales, à condition de créditer la source. + +--- + +## Crédits + +- **Axelle Abbadie** — conception, spécifications, direction du développement ([HAL](https://cv.hal.science/axelle-abbadie/)) +- **Claude Sonnet 4.6** (Anthropic) — co-développement du code via [Claude Code](https://claude.ai/code) +- **Baptiste Coulmont** — outil de suggestion de prénoms sociologiquement équivalents ([coulmont.com](https://coulmont.com)) +- **Alex Alber** (Université de Tours / UMR CITERES) — inspiration de la chaîne Whispurge → Sonal pi pour le workflow de pseudonymisation +- **UMR ICAR** (CNRS / Université Lyon 2 / ENS Lyon) — contexte scientifique et conventions de transcription ICOR diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..acb8a1c --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,215 @@ +# ROADMAP — PseudObsidian-ization + +Plugin Obsidian de pseudonymisation et correction de transcriptions (conventions Jefferson / ICOR / SRT / CHAT). + +Chaque phase produit quelque chose de testable. Les critères d'acceptation renvoient aux sections de SPECS.md. + +## État actuel (mai 2026) + +**Phases 0 à 6 terminées.** La Phase 7 est en cours : l'intégration de l'outil Coulmont est opérationnelle (suggestion de prénoms sociologiquement équivalents via `coulmont.com`). Le travail porte maintenant sur la **constitution de dictionnaires lexicaux embarqués** (prénoms, toponymes, institutions) et leur **intégration dans le moteur de suggestion** pour les phases de scan automatique. + +``` +✅ Phases 0–6 Parsers · Moteur · Commandes UI · Portées · Surlignage · Validation sélective +🔄 Phase 7 Dictionnaires — Coulmont ✅ · Dictionnaires JSON/CSV · Ressources embarquées +⏳ Phase 8 Scan automatique (détection sans règle préexistante) +⏳ Phase 9 Interface complète (panneau latéral 4 onglets) +⏳ Phase 10 Publication communauté Obsidian +``` + +--- + +--- + +## ✅ Phase 0 — Boilerplate + +- [x] Initialiser le plugin avec le template officiel Obsidian (TypeScript + esbuild) +- [x] Configurer ESLint, Prettier, Jest +- [x] Définir les types partagés de SPECS.md §11.3 : `ScopeType`, `MappingStatus`, `EntityCategory`, `MappingRule`, `Scope`, `Occurrence`, `ReplacementSpan` +- [x] Créer un vault de test avec des fichiers fictifs : `entretien_01.srt`, `entretien_02.cha`, `entretien_03.md` +- [x] Mettre en place le dossier `_pseudonymisation/` dans le vault de test (§13.3) + +**Testable :** le plugin se charge dans Obsidian sans erreur. + +--- + +## ✅ Phase 1 — Parser SRT + +Objectif : ouvrir un `.srt`, le lire sans l'altérer, identifier les zones textuelles. Priorité haute car c'est le format le plus courant en sortie de Whisper. + +- [ ] Implémenter `parsers/SrtParser.ts` : + - Découpage en blocs (numéro / horodatage / texte) + - Identification des zones remplaçables (texte uniquement, pas les timestamps) + - Représentation interne : tableau de `SrtBlock { index, start, end, lines[] }` +- [ ] Tests unitaires sur `entretien_01.srt` fictif +- [ ] Vérifier que la reconstruction du fichier à partir de l'AST est identique à l'original (round-trip) + +**Testable — SPECS §17.4 :** après parse + reconstruction, les horodatages et numéros de blocs sont inchangés. + +--- + +## ✅ Phase 2 — Parser CHAT / CHA + +Objectif : ouvrir un `.cha`, distinguer métadonnées, tours de parole et lignes dépendantes. + +- [ ] Implémenter `parsers/ChatParser.ts` : + - Lignes `@` (métadonnées) → préservées telles quelles + - Lignes `*LOCUTEUR:` → tour de parole, zone remplaçable + - Lignes `%` (dépendantes) → préservées ou traitées séparément selon action explicite + - Représentation interne : `ChatLine { type: 'meta'|'turn'|'dependent', speaker?, content }` +- [ ] Tests unitaires sur `entretien_02.cha` fictif +- [ ] Vérifier le round-trip + +**Testable — SPECS §17.5 :** les lignes `@`, `*` et `%` sont conservées après parse + reconstruction. + +--- + +## ✅ Phase 3 — Mapping basique : sélection → règle → application fichier + +Objectif : premier flux complet, portée fichier uniquement. C'est le cœur du MVP v0.1 (SPECS §16.1). + +- [ ] Commande "Créer une règle depuis la sélection" (§10.1) : modale minimale (source, remplacement, catégorie, portée = fichier par défaut) +- [ ] `mappings/MappingStore.ts` : lecture / écriture de `entretien_XX.mapping.json` (portée `file`) +- [ ] `scanner/OccurrenceScanner.ts` : scan du fichier courant, liste des occurrences avec contexte gauche/droite (§7.2) +- [ ] Vue latérale minimale — onglet **Occurrences** : fichier, ligne, contexte, remplacement proposé, bouton valider/ignorer (§10.3) +- [ ] `pseudonymizer/ReplacementPlanner.ts` : construit le plan de remplacement (§12.1) +- [ ] `pseudonymizer/SpanProtector.ts` : résolution des spans, application droite-à-gauche (§12.4, §12.5) +- [ ] Export du fichier pseudonymisé (`nom.pseudonymized.srt` / `.cha` / `.md`) sans table de correspondance (§14.1) +- [ ] Export de la table JSON séparément (§14.2) + +**Testable — SPECS §17.1 :** `Bonjour Jean.` → règle Jean → Pierre → export contient `Bonjour Pierre.` et la table JSON contient le mapping. + +--- + +## ✅ Phase 4 — Priorité z-index et protection des spans imbriqués + +Objectif : garantir qu'un remplacement court ne s'applique pas à l'intérieur d'un segment long déjà traité. + +- [x] Champ `priority` dans `MappingRule` : entier libre, défaut 0 (comme z-index CSS — §8.4) +- [x] `sortRules` : tri par `priority` décroissant, puis longueur source décroissante, puis portée locale (§12.3) +- [x] `resolveSpans` : élimination des chevauchements selon ce tri (§12.4) +- [x] Tests de non-régression obligatoires (§18.2) : Jean/Saint-Jean-de-Luz, Paul/Saint-Paul, Montpellier/CHU, Marie/Sainte-Marie +- [ ] **Exploitable dans Obsidian** : + - Commande `Pseudonymisation : ajouter une transcription` (Ctrl+P) : ouvre un sélecteur de fichier natif, importe le fichier dans le vault, initialise un mapping JSON vide, ouvre le fichier + - Commande `Pseudonymisation : pseudonymiser le fichier courant` : lit le fichier actif (SRT, CHA/CHAT, MD), charge le mapping JSON correspondant, écrit `[nom].pseudonymized.[ext]` dans `_pseudonymisation/exports/` + - Commande `Pseudonymisation : créer une règle` (disponible sur sélection dans l'éditeur) : modale minimale — source, remplacement, catégorie, portée, priority — écrit dans le mapping JSON du fichier courant + - Tester manuellement : importer `entretien_01.srt`, créer les règles Jean → Pierre et Saint-Jean-de-Luz → Ville littorale, pseudonymiser, vérifier l'export + +**Testable — SPECS §17.2 :** `Jean habite Saint-Jean-de-Luz.` → `Pierre habite Ville littorale.` visible dans le fichier exporté dans Obsidian. + +--- + +## ✅ Phase 5 — Portée dossier et vault + surlignage éditeur + +Objectif : charger automatiquement les trois niveaux de mapping et visualiser dans l'éditeur les termes déjà pseudonymisés. + +- [ ] `mappings/ScopeResolver.ts` : parcourir le dossier `_pseudonymisation/mappings/`, charger tous les fichiers JSON, filtrer et fusionner les règles applicables à un fichier donné (§4, §12.2) +- [ ] Cascade de résolution : fichier → dossier le plus proche → vault — géré par le tri `sortRules` existant via `scopeWeight` (§4.4) +- [ ] **Surlignage éditeur** : extension CodeMirror 6 (`registerEditorExtension`) qui marque dans le fichier ouvert : + - en **orange** : les termes sources (encore à pseudonymiser) + - en **vert** : les termes de remplacement (déjà pseudonymisés) + - se met à jour à chaque changement de fichier actif +- [ ] **Exploitable dans Obsidian** : + - La commande "pseudonymiser" utilise ScopeResolver pour charger les trois niveaux automatiquement + - Commande `Pseudonymisation : créer une règle au niveau dossier / vault` + - Le surlignage apparaît dès qu'un mapping existe pour le fichier ouvert + - Tester : une règle vault s'applique à deux entretiens différents ; les termes s'affichent surlignés dans les deux fichiers + +**Testable :** ouvrir un fichier avec des règles de mapping → termes sources en orange, pseudonymes déjà appliqués en vert. + +--- + +## ✅ Phase 6 — Validation sélective et statuts + +Objectif : pouvoir remplacer certaines occurrences et en ignorer d'autres. + +- [ ] Statuts par occurrence : `suggested`, `validated`, `ignored`, `partial`, `conflict`, `disabled`, `needs_review` (§5.4) +- [ ] Validation occurrence par occurrence, par lot, ou globale (§10.5) +- [ ] Mapping au statut `partial` quand certaines occurrences seulement sont remplacées (§17.3) +- [ ] **Exploitable dans Obsidian** : + - Commande `Pseudonymisation : scanner le fichier courant` → liste les occurrences candidates dans une modale + - Chaque occurrence : bouton Valider / Ignorer / Faux positif + - Prévisualisation diff par occurrence avant application (§7.4) + - Menu contextuel sur sélection : Créer une règle / Pseudonymiser cette occurrence (§10.2) + - Tester : trois occurrences de `Jean`, en valider deux, ignorer une → statut `partial` dans le JSON + +**Testable — SPECS §17.3 :** mapping `Jean → Pierre` passe au statut `partial` après validation sélective. + +--- + +## 🔄 Phase 7 — Dictionnaires : import et suggestions + +Objectif : importer des dictionnaires existants (Coulmont, INSEE, etc.) et générer des suggestions de remplacement. + +- [ ] Format interne `DictionaryEntry` avec `gender`, `decade`, `socialClass`, `replacementCandidates[]` (§6.3) +- [ ] `dictionaries/JsonDictionaryImporter.ts` : import JSON format SPECS §6.3 +- [ ] `dictionaries/CsvDictionaryImporter.ts` : import CSV + mapping des colonnes (§6.4) +- [ ] `adapters/coulmont.ts` : CSV Coulmont → `DictionaryEntry[]` +- [ ] `adapters/insee.ts` : CSV INSEE prénoms → `DictionaryEntry[]` +- [ ] `adapters/geoapi.ts` : GeoAPI INSEE (villes + `sizeClass`) → `DictionaryEntry[]` +- [ ] `dictionaries/DictionaryManager.ts` : activation / désactivation, portée par dictionnaire (§6.5) +- [ ] Dictionnaires embarqués : `firstnames.json`, `lastnames.json`, `cities.json`, `ambiguous.json`, `protected.json` +- [ ] **Exploitable dans Obsidian** : + - Commande `Pseudonymisation : importer un dictionnaire` (JSON ou CSV) + - Dans la modale de création de règle : suggestions issues des dictionnaires actifs + - Tester : importer `prenoms_coulmont.json`, créer une règle pour `Jean` → candidats proposés depuis le dictionnaire + +**Testable :** suggestion automatique de remplacement à partir d'un dictionnaire Coulmont importé. + +--- + +## ⏳ Phase 8 — Détection automatique et chevauchements + +Objectif : scanner un fichier sans règle préexistante et détecter les entités candidates. + +- [ ] `scanner/DictionaryScanner.ts` : croisement texte × dictionnaires actifs (§7.2) +- [ ] `scanner/RegexScanner.ts` : heuristiques typographiques (majuscule initiale, locuteurs CHAT, formats Jefferson/ICOR) +- [ ] `mappings/ConflictDetector.ts` : détection des chevauchements (§8.5) +- [ ] Marquage automatique `needs_review` pour les occurrences chevauchantes +- [ ] `ambiguous.json` : tokens ville/prénom ambigus — signalement pour validation manuelle +- [ ] **Exploitable dans Obsidian** : + - Commande `Pseudonymisation : scanner le fichier` sans mapping préexistant → propose des candidats depuis les dictionnaires et les heuristiques + - Modale de désambiguïsation : contexte du tour + boutons Ville / Prénom / Ignorer + - Tester : scanner `entretien_01.cha`, voir `Jean` et `Saint-Jean-de-Luz` détectés, chevauchement signalé + +**Testable :** scan automatique → détection de candidats → résolution manuelle de l'ambiguïté ville/prénom. + +--- + +## ⏳ Phase 9 — Interface complète et correction des conventions + +- [ ] Vue latérale complète : 4 onglets Occurrences / Mappings / Dictionnaires / Exports (§10.3) +- [ ] Onglet **Mappings** : modifier source, remplacement, catégorie, portée, priority (z-index), désactiver +- [ ] Onglet **Exports** : choix format, vérification absence de table dans l'export (§10.4) +- [ ] Rapport de pseudonymisation (§14.3) +- [ ] `correction/checker.ts` : vérification des conventions Jefferson/ICOR +- [ ] Suggestions de correction au survol +- [ ] **Exploitable dans Obsidian** : + - Panneau latéral complet accessible via l'icône ruban + - Workflow complet : ouvrir → scanner → valider → pseudonymiser → exporter sans table + - Tester le workflow de bout en bout sur `entretien_01.srt` et `entretien_02.cha` + +--- + +## ⏳ Phase 10 — Publication + +- [ ] README utilisateur en français avec captures d'écran +- [ ] Guide de démarrage rapide pour les chercheurs ICAR +- [ ] Trancher les questions ouvertes de SPECS §20 +- [ ] Publication sur le répertoire communautaire Obsidian +- [ ] Licence : MIT ou EUPL (selon contraintes CNRS) + +--- + +## Questions ouvertes (SPECS §20) + +| # | Question | Statut | +|---|---|---| +| 1 | Modifier les fichiers originaux ou fonctionner uniquement par export ? | À trancher | +| 2 | Tables de correspondance dans le vault ou hors vault par défaut ? | À trancher | +| 3 | Chiffrement des tables dès la v1 ? | À trancher | +| 4 | Métadonnées CHAT dès le MVP ou à partir de la v0.3 ? | À trancher | +| 5 | NER avancé plus tard, ou rester sur dictionnaires + regex + validation humaine ? | À trancher | +| 6 | Compatibilité exacte à viser avec les JSON de Sonal pi / Whispurge ? | À explorer | +| 7 | Couplage audio optionnel via fichier local (API Obsidian) ? | À explorer | +| 8 | Export ELAN ou Praat ? | À explorer | +| 9 | Liste canonique pour `ambiguous.json` (Nancy, Florence, Lorraine…) ? | À constituer | diff --git a/SPECS.md b/SPECS.md new file mode 100644 index 0000000..32ef858 --- /dev/null +++ b/SPECS.md @@ -0,0 +1,1230 @@ +# SPECS — PseudObsidian-ization + +Spécifications fonctionnelles et techniques — Plugin Obsidian de correction et pseudonymisation de transcriptions + +1. Objectif général + +Développer un plugin Obsidian permettant de corriger, annoter, pseudonymiser et exporter des transcriptions issues de terrains de recherche, en particulier dans des formats structurés comme .srt, .cha / .chat, .txt ou .md. + +Le plugin doit permettre de travailler directement dans Obsidian sur des transcriptions brutes ou semi-brutes, tout en produisant : + +1. une version pseudonymisée exploitable pour l’analyse discursive, interactionnelle ou conversationnelle ; +2. une table de correspondance séparée, sécurisée, permettant éventuellement la réidentification contrôlée ; +3. des dictionnaires de remplacement réutilisables à l’échelle d’un fichier, d’un dossier parent ou d’un vault entier ; +4. une interface de validation permettant de choisir si une occurrence doit être remplacée localement, globalement ou ignorée. + +L’enjeu n’est pas seulement de masquer les données personnelles, mais de préserver autant que possible les propriétés analytiques des énoncés étudiés : structure interactionnelle, indices catégoriels pertinents, formatage conversationnel, rythme, tours de parole, types de toponymes, genres, âges approximatifs, positions institutionnelles, etc. + +⸻ + +2. Cas d’usage principaux + +2.1 Correction simple d’une transcription + +L’utilisateur ouvre une transcription dans Obsidian, sélectionne un mot, un segment ou une séquence, puis demande à le remplacer par une autre forme. + +Exemples : + +* corriger une erreur de reconnaissance automatique ; +* uniformiser une graphie ; +* remplacer un prénom réel par un prénom pseudonymisé ; +* remplacer un lieu précis par une catégorie de lieu ; +* corriger une désignation institutionnelle trop identifiable. + +Le remplacement peut être appliqué : + +* uniquement à l’occurrence sélectionnée ; +* à toutes les occurrences identiques du fichier ; +* à toutes les occurrences du dossier courant ; +* à toutes les occurrences du vault ; +* à une sélection manuelle d’occurrences proposées par le plugin. + +2.2 Pseudonymisation assistée + +Le plugin détecte ou propose des éléments potentiellement identifiants : + +* prénoms ; +* noms de famille ; +* lieux ; +* établissements ; +* villes ; +* services hospitaliers ; +* dates ; +* âges ; +* métiers rares ; +* combinaisons indirectement identifiantes ; +* événements biographiques singuliers. + +L’utilisateur peut valider, ignorer ou modifier les propositions. + +2.3 Travail avec dictionnaires importés + +L’utilisateur peut importer plusieurs dictionnaires de correspondance, par exemple : + +* un dictionnaire de prénoms, inspiré ou dérivé de ressources comme les listes de Baptiste Coulmont ; +* un dictionnaire de noms de lieux ; +* un dictionnaire de catégories institutionnelles ; +* un dictionnaire interne au projet de recherche ; +* un dictionnaire déjà produit lors d’un précédent traitement. + +L’interface permet de choisir quels dictionnaires appliquer au fichier, au dossier ou au vault. + +2.4 Export séparé + +À la fin du traitement, l’utilisateur peut exporter : + +1. la transcription pseudonymisée seule, sans table de correspondance ; +2. la table de correspondance seule ; +3. un rapport de pseudonymisation ; +4. éventuellement un paquet d’export contenant la transcription, le rapport et les métadonnées non sensibles. + +La table de correspondance doit pouvoir être exportée dans un format structuré, notamment JSON, sur le modèle des outils comme Sonal PI. + +⸻ + +3. Formats de transcription à prendre en charge + +3.1 Formats prioritaires + +Le plugin doit prendre en charge en priorité : + +* .srt : sous-titres horodatés ; +* .cha ou .chat : format CHAT / CLAN ; +* .txt : transcription texte simple ; +* .md : transcription annotée dans Obsidian. + +3.2 Contraintes propres au SRT + +Le format SRT contient des blocs de type : + +1 +00:00:01,000 --> 00:00:04,000 +Bonjour Jean, tu es arrivé à Montpellier quand ? + +Le plugin doit préserver : + +* les numéros de blocs ; +* les horodatages ; +* les sauts de ligne ; +* l’ordre des segments ; +* les éventuelles balises ou conventions déjà présentes. + +La pseudonymisation ne doit porter que sur le contenu textuel, sauf action explicite de l’utilisateur. + +3.3 Contraintes propres au CHAT / CLAN + +Le format CHAT contient des lignes structurées : + +@Begin +@Languages: fra +@Participants: INV Investigator, PAR Participant +*INV: Bonjour Jean, tu peux te présenter ? +*PAR: je m'appelle Jean et j'habite Saint-Jean-de-Luz. +@End + +Le plugin doit préserver : + +* les lignes de métadonnées commençant par @ ; +* les identifiants de locuteur commençant par * ; +* les lignes dépendantes commençant par % ; +* les conventions CHAT existantes ; +* les alignements et annotations interactionnelles. + +La pseudonymisation doit être capable de traiter séparément : + +* les métadonnées ; +* les tours de parole ; +* les lignes dépendantes ; +* les identifiants de participants. + +3.4 Préservation des conventions analytiques + +Le plugin ne doit pas dégrader les conventions propres aux analyses AD, EMCA ou linguistiques : + +* pauses ; +* chevauchements ; +* allongements vocaliques ; +* rires ; +* hésitations ; +* marques prosodiques ; +* symboles de transcription ; +* indices de séquentialité ; +* ponctuation analytique. + +Le remplacement doit préserver autant que possible la longueur approximative, la catégorie grammaticale, le genre, le nombre, et la fonction discursive de l’élément remplacé. + +⸻ + +4. Unités de portée + +Le plugin doit gérer trois échelles de correspondance. + +4.1 Portée fichier + +Une table de correspondance est associée à un seul fichier. + +Exemple : + +{ + "scope": "file", + "file": "Entretiens/entretien_01.cha", + "mappings": [ + { + "source": "Jean", + "replacement": "Pierre", + "category": "first_name", + "strategy": "dictionary", + "status": "validated" + } + ] +} + +4.2 Portée dossier parent + +Une table de correspondance est associée à un dossier et peut s’appliquer à tous les fichiers enfants. + +Exemple : + +* Entretiens/2026-03/ +* Entretiens/2026-04/ +* Transcriptions/UJAA/ + +Cela permet de garantir qu’une même personne ou un même lieu soit pseudonymisé de manière cohérente dans tout un sous-corpus. + +4.3 Portée vault + +Une table de correspondance globale s’applique à l’ensemble du vault. + +Cette option est utile pour les terrains longitudinaux, lorsque les mêmes personnes ou lieux réapparaissent dans plusieurs sous-dossiers. + +4.4 Priorité entre portées + +La priorité par défaut doit être : + +1. mapping explicite local sur une occurrence ; +2. mapping fichier ; +3. mapping dossier le plus proche ; +4. mapping vault ; +5. dictionnaire externe ; +6. suggestion automatique. + +Un mapping local ou validé manuellement doit toujours primer sur une suggestion issue d’un dictionnaire. + +⸻ + +5. Tables de correspondance + +5.1 Rôle + +La table de correspondance est le fichier sensible qui relie les formes originales aux formes pseudonymisées. + +Elle doit être stockée séparément de la transcription exportée. + +Elle doit pouvoir être : + +* créée automatiquement ; +* consultée dans une interface dédiée ; +* modifiée manuellement ; +* exportée ; +* réimportée ; +* désactivée temporairement ; +* appliquée à un autre fichier ou dossier. + +5.2 Format JSON recommandé + +Format proposé : + +{ + "schemaVersion": "1.0.0", + "createdAt": "2026-05-11T20:00:00.000Z", + "updatedAt": "2026-05-11T20:10:00.000Z", + "project": "LIIPPS", + "scope": { + "type": "folder", + "path": "Transcriptions/UJAA" + }, + "settings": { + "caseSensitive": false, + "accentSensitive": false, + "wholeWordOnly": true, + "preserveCase": true, + "preserveGender": true, + "preserveAnalyticNotation": true + }, + "mappings": [ + { + "id": "map_000001", + "source": "Jean", + "replacement": "Pierre", + "category": "first_name", + "sourceDictionary": "prenoms_coulmont", + "scope": "folder", + "status": "validated", + "priority": 0, + "createdBy": "user", + "createdAt": "2026-05-11T20:05:00.000Z", + "notes": "Remplacement validé pour le participant principal.", + "occurrences": [ + { + "file": "Transcriptions/UJAA/entretien_01.cha", + "line": 42, + "start": 18, + "end": 22, + "status": "validated" + } + ] + } + ] +} + +5.3 Champs minimaux d’un mapping + +Chaque mapping doit contenir : + +* id : identifiant unique ; +* source : forme originale ; +* replacement : forme pseudonymisée ; +* category : type d’entité ; +* scope : fichier, dossier ou vault ; +* status : proposé, validé, ignoré, conflit, désactivé ; +* priority : entier libre (comme un z-index CSS), défaut 0 — plus le nombre est élevé, plus le mapping est appliqué en priorité ; +* occurrences : liste optionnelle des occurrences repérées. + +5.4 Statuts possibles + +Statuts recommandés : + +* suggested : proposition non validée ; +* validated : remplacement validé ; +* ignored : occurrence ou mapping ignoré ; +* partial : certaines occurrences seulement sont remplacées ; +* conflict : conflit entre plusieurs règles ; +* disabled : mapping conservé mais non appliqué ; +* needs_review : remplacement potentiellement risqué. + +⸻ + +6. Dictionnaires de pseudonymisation + +6.1 Fonction générale + +Les dictionnaires servent à proposer des remplacements cohérents en fonction du type d’entité. + +Ils ne doivent jamais s’appliquer de manière irréversible sans validation ou sans mode d’application explicite. + +6.2 Types de dictionnaires + +Le plugin doit permettre d’importer au moins les types suivants : + +* prénoms ; +* patronymes ; +* villes ; +* départements ; +* régions ; +* pays ; +* établissements ; +* professions ; +* institutions ; +* catégories libres définies par l’utilisateur. + +6.3 Format recommandé pour un dictionnaire + +{ + "schemaVersion": "1.0.0", + "dictionaryId": "prenoms_coulmont", + "label": "Prénoms — Baptiste Coulmont", + "type": "first_name", + "language": "fr", + "source": "imported", + "license": "à renseigner par l’utilisateur", + "entries": [ + { + "value": "Jean", + "gender": "masculine", + "frequencyRank": 12, + "replacementCandidates": ["Pierre", "Paul", "Louis"] + }, + { + "value": "Marie", + "gender": "feminine", + "frequencyRank": 3, + "replacementCandidates": ["Claire", "Anne", "Sophie"] + } + ] +} + +6.4 Import de dictionnaires + +L’interface doit permettre : + +* d’importer un dictionnaire JSON ; +* d’importer un CSV ; +* de mapper les colonnes d’un CSV vers les champs attendus ; +* d’activer ou désactiver un dictionnaire ; +* de choisir son ordre de priorité ; +* de définir sa portée par défaut. + +6.5 Sélection des dictionnaires actifs + +Dans l’interface de traitement, l’utilisateur doit pouvoir sélectionner les dictionnaires à appliquer. + +Exemple d’interface : + +* [x] Prénoms — Baptiste Coulmont +* [x] Villes françaises — catégories INSEE +* [ ] Patronymes — dictionnaire interne +* [x] Institutions — LIIPPS + +Pour chaque dictionnaire, on doit pouvoir choisir : + +* appliquer au fichier courant ; +* appliquer au dossier courant ; +* appliquer au vault ; +* utiliser seulement pour les suggestions ; +* utiliser automatiquement avec validation globale ; +* désactiver. + +⸻ + +7. Détection et remplacement des occurrences + +7.1 Sélection manuelle + +L’utilisateur peut sélectionner un mot ou segment dans l’éditeur Obsidian et ouvrir une commande : + +* Créer une règle de remplacement ; +* Pseudonymiser cette occurrence ; +* Pseudonymiser toutes les occurrences similaires ; +* Chercher les occurrences similaires. + +7.2 Détection automatique + +Le plugin doit pouvoir scanner un fichier, un dossier ou un vault pour détecter des occurrences candidates. + +La détection peut combiner : + +* dictionnaires ; +* expressions régulières ; +* règles typographiques ; +* conventions propres aux formats SRT ou CHAT ; +* listes personnalisées ; +* entités déjà présentes dans une table de correspondance. + +7.3 Occurrences suggérées + +Lorsqu’une entité est repérée, le plugin doit proposer toutes les occurrences candidates à l’utilisateur. + +Exemple : + +Source détectée : Jean +Remplacement proposé : Pierre + +Occurrences : + +Fichier Ligne Contexte Action +entretien_01.cha 42 Bonjour Jean, tu peux... remplacer +entretien_01.cha 87 Saint-Jean-de-Luz vérifier +entretien_02.srt 12 Jean disait que... remplacer + +L’utilisateur doit pouvoir choisir : + +* remplacer cette occurrence ; +* remplacer toutes les occurrences exactes ; +* remplacer toutes sauf certaines ; +* ignorer cette occurrence ; +* marquer comme faux positif ; +* créer une règle spécifique. + +7.4 Prévisualisation + +Avant application, le plugin doit afficher une prévisualisation diff. + +Exemple : + +- Bonjour Jean, tu habites Saint-Jean-de-Luz ? ++ Bonjour Pierre, tu habites Ville moyenne limitrophe ? + +Le diff doit être disponible : + +* par occurrence ; +* par fichier ; +* par lot de remplacement. + +⸻ + +8. Gestion des priorités et conflits + +8.1 Problème à éviter + +Le plugin doit éviter les effets de remplacement naïf. + +Exemple problématique : + +* Jean → Pierre +* Saint-Jean-de-Luz → Ville moyenne limitrophe + +Si le plugin applique d’abord Jean → Pierre, il risque de produire : + +Saint-Pierre-de-Luz + +Cette transformation est dangereuse, car elle peut préserver ou produire un indice de réidentification indirecte. + +8.2 Principe général + +Les remplacements doivent être appliqués selon une stratégie de priorité qui protège les entités longues, composées ou englobantes. + +Règle par défaut : + +1. appliquer les remplacements sur les segments les plus longs ; +2. appliquer ensuite les segments plus courts ; +3. ne jamais appliquer un remplacement court à l’intérieur d’un segment déjà remplacé ; +4. signaler les chevauchements ; +5. demander validation en cas de conflit. + +8.3 Priorité automatique par longueur + +Par défaut, le plugin doit trier les mappings par longueur décroissante de source. + +Exemple : + +1. Saint-Jean-de-Luz → Ville moyenne limitrophe +2. Jean → Pierre + +Résultat attendu : + +Bonjour Pierre, tu habites Ville moyenne limitrophe ? + +et non : + +Bonjour Pierre, tu habites Saint-Pierre-de-Luz ? + +8.4 Priorité explicite + +Chaque mapping doit pouvoir recevoir un champ priority. + +La priorité fonctionne comme le z-index en CSS : c’est un entier libre, positif ou nul, que l’utilisateur assigne manuellement. Il n’existe pas de paliers sémantiques prédéfinis. Un mapping avec priority: 10 passe avant un mapping avec priority: 2 ; un mapping avec priority: 999 passe avant tous les autres. L’utilisateur décide lui-même de l’échelle. + +La valeur par défaut à la création d’un mapping est 0. Deux mappings à priorité égale sont départagés par la longueur de la source (le plus long passe en premier), puis par la portée la plus locale. + +Le tri d’application est : + +1. priorité explicite décroissante (entier libre, défini par l’utilisateur) ; +2. longueur de la source décroissante ; +3. portée la plus locale ; +4. validation manuelle avant suggestion automatique. + +8.5 Chevauchements + +Le plugin doit détecter les chevauchements entre mappings. + +Exemples : + +* Jean dans Saint-Jean-de-Luz ; +* Luz dans Saint-Jean-de-Luz ; +* Paul dans Saint-Paul ; +* CHU dans CHU de Montpellier ; +* Montpellier dans CHU de Montpellier. + +En cas de chevauchement, le plugin doit : + +* marquer les occurrences comme needs_review ; +* proposer l’entité englobante en priorité ; +* empêcher le remplacement partiel non validé ; +* permettre à l’utilisateur de créer une règle composée. + +8.6 Protection des segments déjà remplacés + +Lors de l’application, le plugin doit utiliser une logique de spans protégés. + +Principe : + +1. scanner le texte original ; +2. identifier toutes les occurrences candidates avec leurs positions ; +3. résoudre les conflits ; +4. sélectionner les spans à remplacer ; +5. appliquer les remplacements de droite à gauche ou via reconstruction du texte ; +6. empêcher tout remplacement à l’intérieur d’un span déjà traité. + +⸻ + +9. Préservation analytique des énoncés + +9.1 Problème scientifique + +Dans des corpus destinés à l’analyse du discours, à l’EMCA ou à la linguistique interactionnelle, une pseudonymisation trop brutale peut dégrader l’objet d’analyse. + +Le plugin doit donc viser une pseudonymisation contrôlée, non une anonymisation destructrice. + +9.2 Préservation des catégories pertinentes + +Quand c’est possible, le remplacement doit préserver : + +* le type d’entité ; +* le genre grammatical ou socialement perçu, si pertinent et connu ; +* la catégorie d’âge approximative ; +* la longueur approximative ; +* le registre ; +* la place syntaxique ; +* la structure morphologique ; +* le statut institutionnel ; +* la granularité géographique. + +Exemples : + +* Jean → prénom masculin courant ; +* Marie → prénom féminin courant ; +* Montpellier → ville moyenne ou grande ville du Sud, selon la stratégie choisie ; +* CHU de Montpellier → centre hospitalier universitaire régional ou CHU d’une grande ville du Sud, selon le niveau de généralisation. + +9.3 Stratégies de remplacement + +Le plugin doit proposer plusieurs stratégies : + +1. remplacement par pseudonyme réaliste ; +2. remplacement par catégorie analytique ; +3. remplacement par étiquette neutre ; +4. généralisation ; +5. suppression ; +6. masquage partiel. + +Exemples : + +Source Stratégie Remplacement +Jean prénom réaliste Pierre +Saint-Jean-de-Luz catégorie analytique ville moyenne littorale +CHU de Montpellier généralisation CHU régional +14 mars 2026 généralisation au printemps 2026 +17 ans catégorie mineur·e proche de la majorité + +9.4 Conservation du rythme interactionnel + +Pour les transcriptions fines, le plugin doit éviter d’altérer : + +* le nombre de tours ; +* les retours à la ligne ; +* les pauses ; +* les chevauchements ; +* les signes de prosodie ; +* les notations de rire, souffle, hésitation ; +* les timestamps. + +⸻ + +10. Interface utilisateur Obsidian + +10.1 Commandes principales + +Le plugin doit ajouter les commandes suivantes à la palette Obsidian : + +* Pseudonymisation: créer une règle depuis la sélection ; +* Pseudonymisation: scanner le fichier courant ; +* Pseudonymisation: scanner le dossier courant ; +* Pseudonymisation: scanner le vault ; +* Pseudonymisation: ouvrir la table de correspondance ; +* Pseudonymisation: gérer les dictionnaires ; +* Pseudonymisation: exporter la transcription pseudonymisée ; +* Pseudonymisation: exporter la table de correspondance ; +* Pseudonymisation: générer un rapport de pseudonymisation. + +10.2 Menu contextuel + +Sur une sélection de texte, le clic droit doit proposer : + +* Créer une règle de remplacement ; +* Remplacer cette occurrence ; +* Chercher toutes les occurrences similaires ; +* Marquer comme donnée identifiante ; +* Ignorer cette occurrence. + +10.3 Vue latérale + +Le plugin doit fournir une vue latérale avec quatre onglets : + +1. Occurrences ; +2. Mappings ; +3. Dictionnaires ; +4. Exports. + +Onglet Occurrences + +Affiche les occurrences détectées avec : + +* fichier ; +* ligne ; +* contexte gauche/droite ; +* catégorie supposée ; +* remplacement proposé ; +* statut ; +* action. + +Onglet Mappings + +Affiche les règles validées, avec possibilité de : + +* modifier la source ; +* modifier le remplacement ; +* changer la catégorie ; +* changer la portée ; +* changer la priorité ; +* désactiver la règle ; +* voir les occurrences liées. + +Onglet Dictionnaires + +Permet de : + +* importer un dictionnaire ; +* activer/désactiver un dictionnaire ; +* choisir la portée ; +* définir la priorité ; +* consulter les entrées ; +* tester une suggestion. + +Onglet Exports + +Permet de : + +* choisir le format d’export ; +* choisir le fichier, le dossier ou le vault ; +* exporter la transcription pseudonymisée ; +* exporter la table de correspondance ; +* exporter un rapport ; +* vérifier qu’aucune table de correspondance n’est incluse dans la transcription exportée. + +10.4 Modale de création de règle + +Quand l’utilisateur crée une règle depuis une sélection, une modale doit permettre de renseigner : + +* texte source ; +* remplacement ; +* catégorie ; +* portée ; +* priorité ; +* stratégie ; +* application à cette occurrence seulement ou à plusieurs ; +* recherche d’occurrences similaires. + +10.5 Validation par lots + +Le plugin doit permettre une validation rapide : + +* tout remplacer ; +* tout ignorer ; +* valider un par un ; +* valider toutes les occurrences exactes ; +* valider seulement dans certains fichiers ; +* valider selon contexte. + +⸻ + +11. Architecture technique proposée + +11.1 Stack + +Plugin Obsidian standard : + +* TypeScript ; +* API Obsidian ; +* CodeMirror pour interaction avec l’éditeur ; +* JSON pour tables et dictionnaires ; +* éventuellement Web Workers pour le scan de grands corpus. + +11.2 Modules principaux + +Architecture recommandée : + +src/ + main.ts + settings.ts + types.ts + parsers/ + BaseTranscriptParser.ts + SrtParser.ts + ChatParser.ts + MarkdownParser.ts + PlainTextParser.ts + dictionaries/ + DictionaryManager.ts + CsvDictionaryImporter.ts + JsonDictionaryImporter.ts + mappings/ + MappingStore.ts + MappingResolver.ts + ConflictDetector.ts + ScopeResolver.ts + scanner/ + OccurrenceScanner.ts + RegexScanner.ts + DictionaryScanner.ts + pseudonymizer/ + PseudonymizationEngine.ts + ReplacementPlanner.ts + SpanProtector.ts + ui/ + PseudonymizationView.ts + RuleModal.ts + DictionaryModal.ts + ExportModal.ts + exporters/ + TranscriptExporter.ts + MappingExporter.ts + ReportExporter.ts + tests/ + +11.3 Types principaux + +type ScopeType = 'file' | 'folder' | 'vault'; +type MappingStatus = 'suggested' | 'validated' | 'ignored' | 'partial' | 'conflict' | 'disabled' | 'needs_review'; +type EntityCategory = + | 'first_name' + | 'last_name' + | 'full_name' + | 'place' + | 'institution' + | 'date' + | 'age' + | 'profession' + | 'custom'; +interface MappingRule { + id: string; + source: string; + replacement: string; + category: EntityCategory; + scope: Scope; + status: MappingStatus; + priority: number; + sourceDictionary?: string; + createdBy: 'user' | 'dictionary' | 'scanner'; + createdAt: string; + updatedAt?: string; + notes?: string; +} +interface Scope { + type: ScopeType; + path?: string; +} +interface Occurrence { + id: string; + file: string; + line: number; + start: number; + end: number; + text: string; + contextBefore: string; + contextAfter: string; + category?: EntityCategory; + mappingId?: string; + status: MappingStatus; +} +interface ReplacementSpan { + start: number; + end: number; + source: string; + replacement: string; + mappingId: string; + priority: number; +} + +⸻ + +12. Algorithme de remplacement recommandé + +12.1 Étapes générales + +1. Charger le texte original. +2. Parser le format du fichier. +3. Identifier les zones remplaçables. +4. Charger les mappings applicables selon la portée. +5. Scanner les occurrences candidates. +6. Résoudre les conflits. +7. Construire un plan de remplacement. +8. Afficher une prévisualisation. +9. Appliquer les remplacements validés. +10. Exporter la transcription pseudonymisée. + +12.2 Résolution des mappings applicables + +Pseudo-code : + +function getApplicableMappings(filePath: string): MappingRule[] { + const localMappings = mappingStore.getFileMappings(filePath); + const folderMappings = mappingStore.getFolderMappingsFor(filePath); + const vaultMappings = mappingStore.getVaultMappings(); + return [...localMappings, ...folderMappings, ...vaultMappings] + .filter(mapping => mapping.status === 'validated') + .sort(compareMappings); +} + +12.3 Tri des mappings + +function compareMappings(a: MappingRule, b: MappingRule): number { + if (b.priority !== a.priority) return b.priority - a.priority; + if (b.source.length !== a.source.length) return b.source.length - a.source.length; + return scopeWeight(b.scope) - scopeWeight(a.scope); +} +function scopeWeight(scope: Scope): number { + if (scope.type === 'file') return 3; + if (scope.type === 'folder') return 2; + return 1; +} + +12.4 Protection contre les remplacements imbriqués + +Pseudo-code : + +function resolveSpans(candidates: ReplacementSpan[]): ReplacementSpan[] { + const sorted = candidates.sort((a, b) => { + if (b.priority !== a.priority) return b.priority - a.priority; + if ((b.end - b.start) !== (a.end - a.start)) { + return (b.end - b.start) - (a.end - a.start); + } + return a.start - b.start; + }); + const accepted: ReplacementSpan[] = []; + for (const candidate of sorted) { + const overlaps = accepted.some(span => + candidate.start < span.end && candidate.end > span.start + ); + if (!overlaps) { + accepted.push(candidate); + } + } + return accepted.sort((a, b) => b.start - a.start); +} + +12.5 Application des remplacements + +Appliquer les remplacements de droite à gauche pour éviter de décaler les indices : + +function applySpans(text: string, spans: ReplacementSpan[]): string { + let output = text; + for (const span of spans.sort((a, b) => b.start - a.start)) { + output = output.slice(0, span.start) + span.replacement + output.slice(span.end); + } + return output; +} + +⸻ + +13. Stockage dans le vault + +13.1 Dossier recommandé + +Le plugin peut créer un dossier de configuration dans le vault : + +.obsidian/plugins/transcript-pseudonymizer/ + +Mais les tables sensibles ne devraient pas nécessairement être stockées dans .obsidian, car ce dossier peut être synchronisé automatiquement. + +Il faut prévoir une option permettant de choisir un dossier sécurisé séparé, par exemple : + +_pseudonymisation/ + dictionaries/ + mappings/ + exports/ + reports/ + +Ou, mieux, un chemin hors vault si l’utilisateur le souhaite. + +13.2 Avertissement de sécurité + +Le plugin doit afficher un avertissement si la table de correspondance est stockée dans un dossier susceptible d’être synchronisé, publié ou exporté. + +Exemples : + +* vault synchronisé sur Git ; +* vault publié avec Quartz ; +* dossier partagé ; +* dossier d’export public ; +* dossier inclus dans une sauvegarde non chiffrée. + +13.3 Fichiers générés + +Fichiers recommandés : + +_pseudonymisation/ + dictionaries/ + prenoms_coulmont.json + lieux_france.json + mappings/ + vault.mapping.json + Transcriptions_UJAA.mapping.json + entretien_01.mapping.json + exports/ + entretien_01.pseudonymized.cha + reports/ + entretien_01.report.md + +⸻ + +14. Export + +14.1 Export de la transcription pseudonymisée + +L’export doit préserver le format source quand c’est possible : + +* .srt vers .srt ; +* .cha vers .cha ; +* .chat vers .chat ; +* .txt vers .txt ; +* .md vers .md. + +Nom recommandé : + +nom_du_fichier.pseudonymized.ext + +La transcription exportée ne doit contenir aucune table de correspondance. + +14.2 Export de la table de correspondance + +La table doit pouvoir être exportée en : + +* JSON ; +* CSV ; +* Markdown pour revue humaine ; +* éventuellement format compatible Sonal PI si le schéma exact est renseigné ultérieurement. + +14.3 Export du rapport + +Le rapport doit indiquer : + +* nombre d’occurrences détectées ; +* nombre d’occurrences remplacées ; +* nombre d’occurrences ignorées ; +* nombre de conflits ; +* dictionnaires utilisés ; +* portée du traitement ; +* fichiers concernés ; +* date d’export ; +* paramètres appliqués. + +Le rapport ne doit pas inclure les correspondances sensibles par défaut. + +Une option explicite peut permettre d’inclure les correspondances, mais avec avertissement. + +⸻ + +15. Paramètres du plugin + +15.1 Paramètres généraux + +* dossier de stockage des tables ; +* dossier de stockage des dictionnaires ; +* dossier d’export ; +* mode de confirmation par défaut ; +* sensibilité à la casse ; +* sensibilité aux accents ; +* remplacement mot entier uniquement ; +* préservation de la casse ; +* préservation des formats de transcription ; +* avertissements de sécurité. + +15.2 Paramètres de pseudonymisation + +* stratégie par défaut pour les prénoms ; +* stratégie par défaut pour les lieux ; +* stratégie par défaut pour les dates ; +* stratégie par défaut pour les institutions ; +* priorité par défaut des dictionnaires ; +* validation obligatoire ou non ; +* gestion des chevauchements. + +15.3 Paramètres de sécurité + +* ne jamais exporter les tables avec la transcription ; +* demander confirmation avant export d’une table ; +* avertir si export dans un dossier public ; +* avertir si mapping stocké dans le vault ; +* option de chiffrement éventuelle dans une version ultérieure. + +⸻ + +16. MVP proposé + +16.1 Version 0.1 — MVP utile + +Fonctions minimales : + +1. sélection d’un mot ou segment dans l’éditeur ; +2. création d’un mapping source → remplacement ; +3. choix de la portée : fichier, dossier, vault ; +4. stockage JSON de la table de correspondance ; +5. scan du fichier courant ; +6. liste des occurrences trouvées ; +7. validation occurrence par occurrence ou globale ; +8. application avec priorité manuelle (z-index) puis longueur décroissante en cas d'égalité ; +9. export du fichier pseudonymisé ; +10. export de la table JSON. + +16.2 Version 0.2 — Dictionnaires + +Ajouter : + +1. import JSON ; +2. import CSV ; +3. gestion de plusieurs dictionnaires ; +4. activation/désactivation par dictionnaire ; +5. suggestions de remplacement à partir des dictionnaires ; +6. interface de validation des suggestions. + +16.3 Version 0.3 — Formats spécialisés + +Ajouter : + +1. parser SRT ; +2. parser CHAT ; +3. préservation des zones non textuelles ; +4. export natif .srt et .cha ; +5. rapport de traitement. + +16.4 Version 0.4 — Contrôle avancé des risques + +Ajouter : + +1. détection des chevauchements ; +2. score de risque de réidentification indirecte ; +3. signalement des entités composées ; +4. règles contextuelles ; +5. revue des cas needs_review. + +⸻ + +17. Critères d’acceptation + +17.1 Remplacement simple + +Étant donné un fichier contenant : + +Bonjour Jean. + +Quand l’utilisateur crée la règle Jean → Pierre, alors l’export doit contenir : + +Bonjour Pierre. + +et la table JSON doit contenir le mapping. + +17.2 Remplacement composé prioritaire + +Étant donné : + +Jean habite Saint-Jean-de-Luz. + +Avec les règles : + +* Jean → Pierre ; +* Saint-Jean-de-Luz → Ville moyenne limitrophe. + +Alors le résultat doit être : + +Pierre habite Ville moyenne limitrophe. + +et jamais : + +Pierre habite Saint-Pierre-de-Luz. + +17.3 Validation sélective + +Étant donné trois occurrences de Jean, l’utilisateur doit pouvoir en remplacer deux et en ignorer une. + +Le mapping doit alors passer au statut partial ou conserver des statuts différenciés par occurrence. + +17.4 Préservation SRT + +Étant donné un fichier SRT, les horodatages et numéros de blocs doivent être inchangés après export. + +17.5 Préservation CHAT + +Étant donné un fichier CHAT, les lignes @, * et % doivent être conservées dans leur structure. + +17.6 Export séparé + +L’export de transcription pseudonymisée ne doit contenir aucune correspondance source → remplacement. + +La table de correspondance doit être exportée séparément. + +⸻ + +18. Tests prioritaires + +18.1 Tests unitaires + +* parsing SRT ; +* parsing CHAT ; +* création de mapping ; +* résolution de portée ; +* tri des priorités ; +* détection de chevauchements ; +* application de spans ; +* export JSON ; +* import dictionnaire JSON ; +* import dictionnaire CSV. + +18.2 Tests de non-régression + +Cas à tester systématiquement : + +Jean / Saint-Jean-de-Luz +Paul / Saint-Paul +Montpellier / CHU de Montpellier +Marie / Sainte-Marie +Luz / Saint-Jean-de-Luz + +18.3 Tests sur corpus simulé + +Créer un petit corpus de test : + +Transcriptions/ + entretien_01.srt + entretien_02.cha + entretien_03.md + +Avec plusieurs entités répétées et composées, afin de vérifier la cohérence fichier/dossier/vault. + +⸻ + +19. Prompt initial recommandé pour Claude Code + +Tu vas développer un plugin Obsidian en TypeScript appelé transcript-pseudonymizer. +Objectif : permettre à une chercheuse de corriger et pseudonymiser des transcriptions dans Obsidian, notamment aux formats SRT, CHAT/CHA, TXT et Markdown. +Commence par implémenter le MVP v0.1 décrit dans les spécifications : +- sélection d’un texte dans l’éditeur Obsidian ; +- création d’une règle source → remplacement ; +- choix de la portée fichier/dossier/vault ; +- stockage JSON des mappings ; +- scan du fichier courant ; +- validation des occurrences ; +- application des remplacements avec priorité aux entités longues ; +- export du fichier pseudonymisé ; +- export séparé de la table de correspondance. +Contraintes essentielles : +- ne jamais faire de remplacement imbriqué naïf ; +- toujours protéger les spans déjà remplacés ; +- appliquer les remplacements par priorité décroissante puis longueur décroissante ; +- préserver les formats SRT et CHAT autant que possible ; +- séparer strictement transcription pseudonymisée et table de correspondance ; +- écrire des tests pour le cas Jean / Saint-Jean-de-Luz. +Lis d’abord le fichier de specs complet, puis propose une architecture de fichiers avant de coder. + +⸻ + +20. Points ouverts à trancher + +1. Le plugin doit-il modifier directement les fichiers originaux ou fonctionner uniquement par export ? +2. Les tables de correspondance doivent-elles être stockées dans le vault ou hors vault par défaut ? +3. Faut-il prévoir un chiffrement des tables dès la première version ? +4. Faut-il gérer les métadonnées CHAT dès le MVP ou seulement à partir de la version 0.3 ? +5. Faut-il intégrer un vrai module NER plus tard, ou rester sur dictionnaires + regex + validation humaine ? +6. Quelle compatibilité exacte viser avec les JSON de Sonal PI ? +7. Comment gérer les cas où la pseudonymisation doit conserver une propriété socialement pertinente mais sensible ? + +⸻ + +21. Principe directeur + +Le plugin ne doit pas être conçu comme un simple outil de recherche-remplacement. + +Il doit être conçu comme un environnement de pseudonymisation qualitative contrôlée, adapté aux corpus interactionnels, aux matériaux de terrain sensibles, et aux exigences de recherche en sciences humaines et sociales. + +La règle fondamentale est : + +aucune transformation irréversible, aucun remplacement global non contrôlé, aucune table sensible exportée avec la transcription pseudonymisée. \ No newline at end of file diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..b149e80 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,39 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +const prod = process.argv[2] === "production"; + +const context = await esbuild.context({ + entryPoints: ["src/main.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins, + ], + format: "cjs", + target: "es2018", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..202687a --- /dev/null +++ b/jest.config.js @@ -0,0 +1,18 @@ +/** @type {import('jest').Config} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/tests'], + testMatch: ['**/*.test.ts'], + moduleNameMapper: { + '^obsidian$': '/tests/__mocks__/obsidian.ts', + }, + globals: { + 'ts-jest': { + tsconfig: { + module: 'commonjs', + strict: true, + }, + }, + }, +}; diff --git a/main.js b/main.js new file mode 100644 index 0000000..c1938b4 --- /dev/null +++ b/main.js @@ -0,0 +1,1549 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/main.ts +var main_exports = {}; +__export(main_exports, { + default: () => PseudObsPlugin +}); +module.exports = __toCommonJS(main_exports); +var import_obsidian7 = require("obsidian"); + +// src/settings.ts +var import_obsidian = require("obsidian"); +var DEFAULT_SETTINGS = { + transcriptionsFolder: "Transcriptions", + mappingFolder: "_pseudonymisation/mappings", + dictionariesFolder: "_pseudonymisation/dictionaries", + exportsFolder: "_pseudonymisation/exports", + reportsFolder: "_pseudonymisation/reports", + caseSensitive: false, + accentSensitive: false, + wholeWordOnly: true, + preserveCase: true, + preserveAnalyticNotation: true, + warnIfSyncedFolder: true, + useMarkerInExport: false, + markerOpen: "\u27E6", + markerClose: "\u27E7" +}; +var PseudObsSettingTab = class extends import_obsidian.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + display() { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl("h2", { text: "PseudObsidian-ization" }); + containerEl.createEl("h3", { text: "Dossiers" }); + new import_obsidian.Setting(containerEl).setName("Transcriptions import\xE9es").setDesc('Dossier de destination pour les fichiers ajout\xE9s via la commande "Ajouter une transcription"').addText( + (text) => text.setValue(this.plugin.settings.transcriptionsFolder).onChange(async (value) => { + this.plugin.settings.transcriptionsFolder = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian.Setting(containerEl).setName("Tables de correspondance").setDesc("Chemin relatif dans le vault").addText( + (text) => text.setValue(this.plugin.settings.mappingFolder).onChange(async (value) => { + this.plugin.settings.mappingFolder = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian.Setting(containerEl).setName("Dictionnaires").setDesc("Chemin relatif dans le vault").addText( + (text) => text.setValue(this.plugin.settings.dictionariesFolder).onChange(async (value) => { + this.plugin.settings.dictionariesFolder = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian.Setting(containerEl).setName("Exports").addText( + (text) => text.setValue(this.plugin.settings.exportsFolder).onChange(async (value) => { + this.plugin.settings.exportsFolder = value; + await this.plugin.saveSettings(); + }) + ); + containerEl.createEl("h3", { text: "Remplacement" }); + new import_obsidian.Setting(containerEl).setName("Sensible \xE0 la casse").addToggle( + (toggle) => toggle.setValue(this.plugin.settings.caseSensitive).onChange(async (value) => { + this.plugin.settings.caseSensitive = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian.Setting(containerEl).setName("Sensible aux accents").addToggle( + (toggle) => toggle.setValue(this.plugin.settings.accentSensitive).onChange(async (value) => { + this.plugin.settings.accentSensitive = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian.Setting(containerEl).setName("Mots entiers uniquement").addToggle( + (toggle) => toggle.setValue(this.plugin.settings.wholeWordOnly).onChange(async (value) => { + this.plugin.settings.wholeWordOnly = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian.Setting(containerEl).setName("Pr\xE9server la casse").setDesc("Adapter la casse du remplacement \xE0 celle de la source (Jean \u2192 Pierre, JEAN \u2192 PIERRE)").addToggle( + (toggle) => toggle.setValue(this.plugin.settings.preserveCase).onChange(async (value) => { + this.plugin.settings.preserveCase = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian.Setting(containerEl).setName("Pr\xE9server les notations analytiques").setDesc("Ne jamais remplacer les symboles Jefferson / ICOR").addToggle( + (toggle) => toggle.setValue(this.plugin.settings.preserveAnalyticNotation).onChange(async (value) => { + this.plugin.settings.preserveAnalyticNotation = value; + await this.plugin.saveSettings(); + }) + ); + containerEl.createEl("h3", { text: "Marqueur d'export" }); + new import_obsidian.Setting(containerEl).setName("Ajouter un marqueur autour des pseudonymes dans l'export").setDesc("Permet d'identifier visuellement les termes pseudonymis\xE9s dans le fichier export\xE9").addToggle( + (toggle) => toggle.setValue(this.plugin.settings.useMarkerInExport).onChange(async (value) => { + this.plugin.settings.useMarkerInExport = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian.Setting(containerEl).setName("Marqueur ouvrant").setDesc("Exemple : \u27E6 [ { \xAB").addText( + (text) => text.setValue(this.plugin.settings.markerOpen).onChange(async (value) => { + this.plugin.settings.markerOpen = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian.Setting(containerEl).setName("Marqueur fermant").setDesc("Exemple : \u27E7 ] } \xBB").addText( + (text) => text.setValue(this.plugin.settings.markerClose).onChange(async (value) => { + this.plugin.settings.markerClose = value; + await this.plugin.saveSettings(); + }) + ); + containerEl.createEl("h3", { text: "S\xE9curit\xE9" }); + new import_obsidian.Setting(containerEl).setName("Avertir si le dossier est synchronis\xE9").setDesc("Alerter si les tables de correspondance sont dans un dossier Git, iCloud ou Synology Drive").addToggle( + (toggle) => toggle.setValue(this.plugin.settings.warnIfSyncedFolder).onChange(async (value) => { + this.plugin.settings.warnIfSyncedFolder = value; + await this.plugin.saveSettings(); + }) + ); + } +}; + +// src/ui/RuleModal.ts +var import_obsidian2 = require("obsidian"); + +// src/mappings/MappingStore.ts +function generateId() { + return `map_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`; +} +var DEFAULT_FILE_SETTINGS = { + caseSensitive: false, + accentSensitive: false, + wholeWordOnly: true, + preserveCase: true, + preserveGender: true, + preserveAnalyticNotation: true +}; +var MappingStore = class _MappingStore { + constructor(scope, project, settings) { + this.rules = /* @__PURE__ */ new Map(); + this.scope = scope; + this.project = project; + this.createdAt = (/* @__PURE__ */ new Date()).toISOString(); + this.updatedAt = this.createdAt; + this.fileSettings = { ...DEFAULT_FILE_SETTINGS, ...settings }; + } + add(partial) { + const rule = { + ...partial, + id: generateId(), + createdAt: (/* @__PURE__ */ new Date()).toISOString() + }; + this.rules.set(rule.id, rule); + this.touch(); + return rule; + } + get(id) { + return this.rules.get(id); + } + update(id, changes) { + const rule = this.rules.get(id); + if (!rule) + return false; + this.rules.set(id, { ...rule, ...changes, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }); + this.touch(); + return true; + } + remove(id) { + const deleted = this.rules.delete(id); + if (deleted) + this.touch(); + return deleted; + } + getAll() { + return Array.from(this.rules.values()); + } + // Règles validées applicables à un fichier donné (cascade file → folder → vault) + getValidatedFor(filePath) { + return this.getAll().filter((r) => { + var _a; + if (r.status !== "validated") + return false; + if (r.scope.type === "vault") + return true; + if (r.scope.type === "folder") + return filePath.startsWith((_a = r.scope.path) != null ? _a : ""); + return r.scope.path === filePath; + }); + } + get settings() { + return this.fileSettings; + } + toJSON() { + return { + schemaVersion: "1.0.0", + createdAt: this.createdAt, + updatedAt: this.updatedAt, + project: this.project, + scope: this.scope, + settings: this.fileSettings, + mappings: this.getAll() + }; + } + static fromJSON(data) { + const store = new _MappingStore(data.scope, data.project, data.settings); + for (const rule of data.mappings) { + store.rules.set(rule.id, rule); + } + return store; + } + touch() { + this.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); + } +}; + +// src/ui/RuleModal.ts +var RuleModal = class extends import_obsidian2.Modal { + constructor(app, plugin, prefillSource = "", prefillReplacement = "", suggestions = []) { + super(app); + this.scopeType = "file"; + this.priority = 0; + this.plugin = plugin; + this.source = prefillSource; + this.replacement = prefillReplacement; + this.suggestions = suggestions; + this.category = suggestions.length > 0 ? "first_name" : "custom"; + } + onOpen() { + const { contentEl } = this; + contentEl.createEl("h2", { text: "Cr\xE9er une r\xE8gle de remplacement" }); + new import_obsidian2.Setting(contentEl).setName("Source").setDesc("Terme original \xE0 remplacer").addText( + (t) => t.setValue(this.source).onChange((v) => { + this.source = v; + }) + ); + let replacementInput; + if (this.suggestions.length > 0) { + const box = contentEl.createDiv(); + box.style.cssText = "margin-bottom:6px;"; + box.createEl("small", { text: "Suggestions Coulmont (M/F non diff\xE9renci\xE9 \u2014 choisissez) :" }).style.cssText = "display:block;opacity:.6;margin-bottom:4px;font-size:.8em;"; + const tags = box.createDiv(); + tags.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px;"; + for (const name of this.suggestions) { + const btn = tags.createEl("button", { text: name }); + btn.style.cssText = "padding:2px 10px;border-radius:12px;border:1px solid var(--background-modifier-border);cursor:pointer;font-size:.85em;background:var(--background-secondary);"; + btn.addEventListener("click", () => { + this.replacement = name; + if (replacementInput) + replacementInput.value = name; + tags.querySelectorAll("button").forEach((b) => { + b.style.cssText = "padding:2px 10px;border-radius:12px;border:1px solid var(--background-modifier-border);cursor:pointer;font-size:.85em;background:var(--background-secondary);"; + }); + btn.style.cssText = "padding:2px 10px;border-radius:12px;border:1px solid var(--interactive-accent);cursor:pointer;font-size:.85em;background:var(--interactive-accent);color:var(--text-on-accent);font-weight:600;"; + }); + } + } + new import_obsidian2.Setting(contentEl).setName("Remplacement").setDesc("Pseudonyme ou cat\xE9gorie analytique").addText((t) => { + t.setValue(this.replacement).onChange((v) => { + this.replacement = v; + }); + replacementInput = t.inputEl; + }); + new import_obsidian2.Setting(contentEl).setName("Cat\xE9gorie").addDropdown((d) => { + var _a; + const options = { + first_name: "Pr\xE9nom", + last_name: "Nom de famille", + full_name: "Nom complet", + place: "Lieu", + institution: "Institution", + date: "Date", + age: "\xC2ge", + profession: "Profession", + custom: "Autre" + }; + for (const [value, label] of Object.entries(options)) { + d.addOption(value, label); + } + d.setValue(this.category); + d.onChange((v) => { + this.category = v; + }); + if (this.suggestions.length > 0) { + (_a = d.selectEl.closest(".setting-item")) == null ? void 0 : _a.setAttribute("style", "display:none"); + } + }); + new import_obsidian2.Setting(contentEl).setName("Port\xE9e").addDropdown((d) => { + d.addOption("file", "Ce fichier uniquement"); + d.addOption("folder", "Ce dossier"); + d.addOption("vault", "Tout le vault"); + d.setValue("file"); + d.onChange((v) => { + this.scopeType = v; + }); + }); + new import_obsidian2.Setting(contentEl).setName("Priorit\xE9").setDesc("Entier libre, comme un z-index CSS \u2014 d\xE9faut 0, plus grand = appliqu\xE9 en premier").addText( + (t) => t.setValue("0").onChange((v) => { + this.priority = parseInt(v, 10) || 0; + }) + ); + new import_obsidian2.Setting(contentEl).addButton( + (btn) => btn.setButtonText("Cr\xE9er la r\xE8gle").setCta().onClick(() => this.createRule()) + ); + } + async createRule() { + var _a, _b; + if (!this.source.trim() || !this.replacement.trim()) { + new import_obsidian2.Notice("La source et le remplacement sont obligatoires."); + return; + } + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + new import_obsidian2.Notice("Aucun fichier actif."); + return; + } + const mappingPath = `${this.plugin.settings.mappingFolder}/${activeFile.basename}.mapping.json`; + let store; + const mappingFile = this.app.vault.getAbstractFileByPath(mappingPath); + if (mappingFile instanceof import_obsidian2.TFile) { + const data = JSON.parse(await this.app.vault.read(mappingFile)); + store = MappingStore.fromJSON(data); + } else { + await this.plugin.ensureFolder(this.plugin.settings.mappingFolder); + store = new MappingStore({ type: "file", path: activeFile.path }); + } + const scopePath = this.scopeType === "file" ? activeFile.path : this.scopeType === "folder" ? (_b = (_a = activeFile.parent) == null ? void 0 : _a.path) != null ? _b : "" : void 0; + store.add({ + source: this.source.trim(), + replacement: this.replacement.trim(), + category: this.category, + scope: { type: this.scopeType, path: scopePath }, + status: "validated", + priority: this.priority, + createdBy: "user" + }); + const json = JSON.stringify(store.toJSON(), null, 2); + if (mappingFile instanceof import_obsidian2.TFile) { + await this.app.vault.modify(mappingFile, json); + } else { + await this.app.vault.create(mappingPath, json); + } + new import_obsidian2.Notice(`\u2713 R\xE8gle cr\xE9\xE9e : "${this.source.trim()}" \u2192 "${this.replacement.trim()}"`); + this.plugin.refreshHighlightData(); + this.close(); + } + onClose() { + this.contentEl.empty(); + } +}; + +// src/ui/QuickPseudonymizeModal.ts +var import_obsidian3 = require("obsidian"); +var QuickPseudonymizeModal = class extends import_obsidian3.Modal { + constructor(app, plugin, editor, prefillReplacement = "", suggestions = []) { + super(app); + this.replacement = ""; + this.category = "custom"; + this.applyScope = "file"; + this.plugin = plugin; + this.editor = editor; + this.source = editor.getSelection(); + this.replacement = prefillReplacement; + this.suggestions = suggestions; + if (suggestions.length > 0) + this.category = "first_name"; + this.from = editor.getCursor("from"); + this.to = editor.getCursor("to"); + } + onOpen() { + const { contentEl } = this; + contentEl.createEl("h2", { text: "Pseudonymiser" }); + new import_obsidian3.Setting(contentEl).setName("Expression s\xE9lectionn\xE9e").setDesc("Terme \xE0 remplacer \u2014 non modifiable").addText((t) => { + t.setValue(this.source).setDisabled(true); + t.inputEl.style.opacity = "0.6"; + }); + let replacementInput; + if (this.suggestions.length > 0) { + const suggBox = contentEl.createDiv(); + suggBox.style.cssText = "margin-bottom:6px;"; + suggBox.createEl("small", { text: "Suggestions Coulmont (le jeu de donn\xE9es ne diff\xE9rencie pas M/F) :" }).style.cssText = "display:block;opacity:.6;margin-bottom:4px;font-size:.8em;"; + const tags = suggBox.createDiv(); + tags.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;"; + for (const name of this.suggestions) { + const btn = tags.createEl("button", { text: name }); + btn.style.cssText = "padding:2px 10px;border-radius:12px;border:1px solid var(--background-modifier-border);cursor:pointer;font-size:.85em;background:var(--background-secondary);"; + btn.addEventListener("click", () => { + this.replacement = name; + if (replacementInput) { + replacementInput.value = name; + replacementInput.dispatchEvent(new Event("input")); + } + tags.querySelectorAll("button").forEach((b) => { + b.style.background = "var(--background-secondary)"; + b.style.fontWeight = "normal"; + }); + btn.style.background = "var(--interactive-accent)"; + btn.style.color = "var(--text-on-accent)"; + btn.style.fontWeight = "600"; + }); + } + } + new import_obsidian3.Setting(contentEl).setName("Remplacer par").addText((t) => { + t.setPlaceholder("pseudonyme ou cat\xE9gorie analytique"); + t.setValue(this.replacement); + t.onChange((v) => this.replacement = v); + replacementInput = t.inputEl; + }); + new import_obsidian3.Setting(contentEl).setName("Cat\xE9gorie").addDropdown((d) => { + const options = { + first_name: "Pr\xE9nom", + last_name: "Nom de famille", + full_name: "Nom complet", + place: "Lieu", + institution: "Institution", + date: "Date", + age: "\xC2ge", + profession: "Profession", + custom: "Autre" + }; + for (const [value, label] of Object.entries(options)) { + d.addOption(value, label); + } + d.setValue("custom"); + d.onChange((v) => this.category = v); + }); + new import_obsidian3.Setting(contentEl).setName("Port\xE9e du remplacement").addDropdown((d) => { + d.addOption("file", "Toutes les occurrences dans ce fichier"); + d.addOption("occurrence", "Cette occurrence uniquement"); + d.setValue("file"); + d.onChange((v) => this.applyScope = v); + }); + new import_obsidian3.Setting(contentEl).addButton( + (btn) => btn.setButtonText("Pseudonymiser").setCta().onClick(() => this.apply()) + ); + setTimeout(() => replacementInput == null ? void 0 : replacementInput.focus(), 50); + } + async apply() { + const replacement = this.replacement.trim(); + if (!replacement) { + new import_obsidian3.Notice("Le remplacement est obligatoire."); + return; + } + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + new import_obsidian3.Notice("Aucun fichier actif."); + return; + } + await this.saveRule(activeFile, replacement); + if (this.applyScope === "occurrence") { + this.editor.replaceRange(replacement, this.from, this.to); + new import_obsidian3.Notice(`\u2713 "${this.source}" \u2192 "${replacement}" (cette occurrence)`); + } else { + const count = await this.plugin.applyRuleToFile(activeFile, this.source, replacement); + new import_obsidian3.Notice(`\u2713 "${this.source}" \u2192 "${replacement}" (${count} occurrence${count > 1 ? "s" : ""})`); + } + this.plugin.refreshHighlightData(); + this.close(); + } + async saveRule(activeFile, replacement) { + const mappingPath = `${this.plugin.settings.mappingFolder}/${activeFile.basename}.mapping.json`; + let store; + const mappingTFile = this.app.vault.getAbstractFileByPath(mappingPath); + if (mappingTFile instanceof import_obsidian3.TFile) { + const data = JSON.parse(await this.app.vault.read(mappingTFile)); + store = MappingStore.fromJSON(data); + } else { + await this.plugin.ensureFolder(this.plugin.settings.mappingFolder); + store = new MappingStore({ type: "file", path: activeFile.path }); + } + store.add({ + source: this.source, + replacement, + category: this.category, + scope: { type: "file", path: activeFile.path }, + status: "validated", + priority: 0, + createdBy: "user" + }); + const json = JSON.stringify(store.toJSON(), null, 2); + if (mappingTFile instanceof import_obsidian3.TFile) { + await this.app.vault.modify(mappingTFile, json); + } else { + await this.app.vault.create(mappingPath, json); + } + } + onClose() { + this.contentEl.empty(); + } +}; + +// src/ui/PseudonymHighlighter.ts +var import_state = require("@codemirror/state"); +var import_view = require("@codemirror/view"); +var highlightDataChanged = import_state.StateEffect.define(); +function createPseudonymHighlighter(getData) { + return import_view.ViewPlugin.fromClass( + class { + constructor(view) { + this.decorations = import_view.Decoration.none; + this.decorations = this.build(view); + } + update(update) { + const dataChanged = update.transactions.some( + (t) => t.effects.some((e) => e.is(highlightDataChanged)) + ); + if (update.docChanged || update.viewportChanged || dataChanged) { + this.decorations = this.build(update.view); + } + } + build(view) { + const { sources, replacements } = getData(); + if (sources.length === 0 && replacements.length === 0) + return import_view.Decoration.none; + const text = view.state.doc.toString(); + const lower = text.toLowerCase(); + const spans = []; + const collect = (terms, cls) => { + for (const term of terms) { + if (!term) + continue; + const needle = term.toLowerCase(); + let pos = 0; + while (pos < lower.length) { + const idx = lower.indexOf(needle, pos); + if (idx === -1) + break; + spans.push({ from: idx, to: idx + term.length, cls }); + pos = idx + term.length; + } + } + }; + collect(sources, "pseudobs-source"); + collect(replacements, "pseudobs-replaced"); + spans.sort((a, b) => a.from - b.from || a.to - b.to); + const builder = new import_state.RangeSetBuilder(); + let lastTo = -1; + for (const { from, to, cls } of spans) { + if (from >= lastTo) { + builder.add(from, to, import_view.Decoration.mark({ class: cls })); + lastTo = to; + } + } + return builder.finish(); + } + }, + { decorations: (v) => v.decorations } + ); +} + +// src/ui/EditRuleModal.ts +var import_obsidian4 = require("obsidian"); +var EditRuleModal = class extends import_obsidian4.Modal { + constructor(app, plugin, location) { + super(app); + this.plugin = plugin; + this.location = location; + const { rule } = location; + this.replacement = rule.replacement; + this.category = rule.category; + this.scopeType = rule.scope.type; + this.priority = rule.priority; + } + onOpen() { + const { contentEl } = this; + const { rule } = this.location; + contentEl.createEl("h2", { text: "Modifier la r\xE8gle" }); + new import_obsidian4.Setting(contentEl).setName("Source").setDesc("Non modifiable \u2014 cr\xE9ez une nouvelle r\xE8gle pour changer la source").addText((t) => { + t.setValue(rule.source).setDisabled(true); + t.inputEl.style.opacity = "0.6"; + }); + new import_obsidian4.Setting(contentEl).setName("Remplacement").addText( + (t) => t.setValue(this.replacement).onChange((v) => this.replacement = v) + ); + new import_obsidian4.Setting(contentEl).setName("Cat\xE9gorie").addDropdown((d) => { + const options = { + first_name: "Pr\xE9nom", + last_name: "Nom de famille", + full_name: "Nom complet", + place: "Lieu", + institution: "Institution", + date: "Date", + age: "\xC2ge", + profession: "Profession", + custom: "Autre" + }; + for (const [value, label] of Object.entries(options)) + d.addOption(value, label); + d.setValue(this.category); + d.onChange((v) => this.category = v); + }); + new import_obsidian4.Setting(contentEl).setName("Port\xE9e").addDropdown((d) => { + d.addOption("file", "Ce fichier uniquement"); + d.addOption("folder", "Ce dossier"); + d.addOption("vault", "Tout le vault"); + d.setValue(this.scopeType); + d.onChange((v) => this.scopeType = v); + }); + new import_obsidian4.Setting(contentEl).setName("Priorit\xE9").setDesc("Entier libre, comme un z-index CSS \u2014 d\xE9faut 0").addText( + (t) => t.setValue(String(this.priority)).onChange((v) => this.priority = parseInt(v, 10) || 0) + ); + new import_obsidian4.Setting(contentEl).addButton( + (btn) => btn.setButtonText("Enregistrer").setCta().onClick(() => this.save()) + ).addButton( + (btn) => btn.setButtonText("Supprimer la r\xE8gle").setWarning().onClick(() => this.delete()) + ); + } + async save() { + if (!this.replacement.trim()) { + new import_obsidian4.Notice("Le remplacement est obligatoire."); + return; + } + const { store, filePath, rule } = this.location; + store.update(rule.id, { + replacement: this.replacement.trim(), + category: this.category, + scope: { ...rule.scope, type: this.scopeType }, + priority: this.priority + }); + await this.plugin.scopeResolver.saveStore(store, filePath); + new import_obsidian4.Notice(`\u2713 R\xE8gle mise \xE0 jour : "${rule.source}" \u2192 "${this.replacement.trim()}"`); + this.plugin.refreshHighlightData(); + this.close(); + } + async delete() { + const { store, filePath, rule } = this.location; + store.remove(rule.id); + await this.plugin.scopeResolver.saveStore(store, filePath); + new import_obsidian4.Notice(`\u2713 R\xE8gle supprim\xE9e : "${rule.source}"`); + this.plugin.refreshHighlightData(); + this.close(); + } + onClose() { + this.contentEl.empty(); + } +}; + +// src/ui/OccurrencesModal.ts +var import_obsidian5 = require("obsidian"); + +// src/pseudonymizer/SpanProtector.ts +function resolveSpans(candidates) { + const sorted = [...candidates].sort((a, b) => { + if (b.priority !== a.priority) + return b.priority - a.priority; + const lenB = b.end - b.start; + const lenA = a.end - a.start; + if (lenB !== lenA) + return lenB - lenA; + return a.start - b.start; + }); + const accepted = []; + for (const candidate of sorted) { + const overlaps = accepted.some( + (s) => candidate.start < s.end && candidate.end > s.start + ); + if (!overlaps) + accepted.push(candidate); + } + return accepted.sort((a, b) => b.start - a.start); +} +function applySpans(text, spans, marker) { + let output = text; + for (const span of spans) { + const value = marker ? `${marker.open}${span.replacement}${marker.close}` : span.replacement; + output = output.slice(0, span.start) + value + output.slice(span.end); + } + return output; +} + +// src/ui/OccurrencesModal.ts +var OccurrencesModal = class extends import_obsidian5.Modal { + constructor(app, plugin, file, content, occurrences, rules) { + super(app); + this.decisions = /* @__PURE__ */ new Map(); + // Références stables — on ne recrée jamais les cartes + this.cardRefs = /* @__PURE__ */ new Map(); + this.plugin = plugin; + this.file = file; + this.content = content; + this.occurrences = occurrences; + this.rules = rules; + for (const occ of occurrences) + this.decisions.set(occ.id, "validated"); + } + onOpen() { + const { contentEl } = this; + contentEl.createEl("h2", { text: `Scanner \u2014 ${this.file.name}` }); + const n = this.occurrences.length; + contentEl.createEl("p", { + text: `${n} occurrence${n > 1 ? "s" : ""} trouv\xE9e${n > 1 ? "s" : ""} pour ${this.countRules()} r\xE8gle${this.countRules() > 1 ? "s" : ""}.` + }); + const legend = contentEl.createDiv(); + legend.style.cssText = "display:flex;gap:16px;font-size:.8em;opacity:.65;margin-bottom:8px;flex-wrap:wrap;"; + for (const [icon, label, color] of [ + ["\u2713", "Valider le remplacement", "rgba(50,205,90,.7)"], + ["\u2717", "Conserver l'original", "rgba(150,150,150,.7)"], + ["\u26A0", "Faux positif \u2014 exclure", "rgba(255,80,80,.6)"] + ]) { + const item = legend.createSpan(); + item.style.cssText = `display:inline-flex;align-items:center;gap:4px;`; + const badge = item.createSpan({ text: icon }); + badge.style.cssText = `background:${color};border-radius:3px;padding:0 5px;font-weight:700;`; + item.createSpan({ text: ` ${label}` }); + } + new import_obsidian5.Setting(contentEl).addButton( + (b) => b.setButtonText("\u2713 Tout valider").onClick(() => { + for (const occ of this.occurrences) + this.decisions.set(occ.id, "validated"); + this.updateAllCards(); + }) + ).addButton( + (b) => b.setButtonText("\u2717 Tout ignorer").onClick(() => { + for (const occ of this.occurrences) + this.decisions.set(occ.id, "ignored"); + this.updateAllCards(); + }) + ); + this.renderAllCards(contentEl); + contentEl.createEl("hr"); + new import_obsidian5.Setting(contentEl).addButton( + (b) => b.setButtonText("Appliquer").setCta().onClick(() => this.apply()) + ); + } + countRules() { + return new Set(this.occurrences.map((o) => o.mappingId)).size; + } + // Construit toutes les cartes une seule fois — ne sera plus jamais appelé après + renderAllCards(container) { + var _a; + const byRule = /* @__PURE__ */ new Map(); + for (const occ of this.occurrences) { + const key = (_a = occ.mappingId) != null ? _a : ""; + if (!byRule.has(key)) + byRule.set(key, []); + byRule.get(key).push(occ); + } + for (const [mappingId, occs] of byRule) { + const rule = this.rules.find((r) => r.id === mappingId); + if (!rule) + continue; + const group = container.createDiv(); + group.createEl("div", { + text: `${rule.source} \u2192 ${rule.replacement}` + }).style.cssText = "font-weight:600;font-size:.9em;padding:4px 8px;background:var(--background-secondary);border-radius:4px;margin:12px 0 6px;"; + for (const occ of occs) { + this.buildCard(group, occ, rule); + } + } + } + buildCard(container, occ, rule) { + var _a; + const decision = (_a = this.decisions.get(occ.id)) != null ? _a : "validated"; + const card = container.createDiv(); + this.applyCardStyle(card, decision); + const srcLine = card.createDiv(); + srcLine.style.cssText = "font-family:var(--font-monospace);font-size:.85em;line-height:1.7;white-space:pre-wrap;word-break:break-word;"; + this.ctxSpan(srcLine, occ.contextBefore); + const termSpan = srcLine.createSpan({ text: occ.text }); + termSpan.style.cssText = "background:rgba(255,210,0,.6);border-radius:3px;padding:1px 5px;font-weight:700;"; + this.ctxSpan(srcLine, occ.contextAfter); + const arrow = card.createDiv(); + arrow.style.cssText = "font-size:.75em;opacity:.35;line-height:1.2;margin:1px 0;user-select:none;"; + arrow.setText("\u2193"); + const resLine = card.createDiv(); + resLine.style.cssText = "font-family:var(--font-monospace);font-size:.85em;line-height:1.7;white-space:pre-wrap;word-break:break-word;opacity:.8;"; + this.ctxSpan(resLine, occ.contextBefore); + const replSpan = resLine.createSpan({ text: rule.replacement }); + replSpan.style.cssText = "background:rgba(50,205,90,.55);border-radius:3px;padding:1px 5px;font-weight:700;"; + this.ctxSpan(resLine, occ.contextAfter); + const statusLabel = card.createDiv(); + statusLabel.style.cssText = "font-size:.8em;font-style:italic;opacity:.55;margin:2px 0 4px;display:none;"; + const meta = card.createEl("small"); + meta.style.cssText = "display:block;font-size:.75em;opacity:.45;margin-top:4px;"; + meta.setText(`ligne ${occ.line}`); + const actions = card.createDiv(); + actions.style.cssText = "display:flex;gap:6px;margin-top:6px;"; + const btnRefs = /* @__PURE__ */ new Map(); + for (const [label, value, title] of [ + ["\u2713", "validated", "Valider"], + ["\u2717", "ignored", "Ignorer"], + ["\u26A0", "false_positive", "Faux positif"] + ]) { + const btn = actions.createEl("button", { text: label }); + btn.title = title; + this.applyBtnStyle(btn, value === decision); + btn.addEventListener("click", () => { + this.decisions.set(occ.id, value); + this.updateCard(occ.id); + }); + btnRefs.set(value, btn); + } + this.cardRefs.set(occ.id, { card, buttons: btnRefs, arrow, resLine, statusLabel }); + this.updateCard(occ.id); + } + // Met à jour UNE carte sans toucher au DOM — styles + résultat en direct + updateCard(occId) { + var _a; + const ref = this.cardRefs.get(occId); + if (!ref) + return; + const decision = (_a = this.decisions.get(occId)) != null ? _a : "validated"; + this.applyCardStyle(ref.card, decision); + for (const [value, btn] of ref.buttons) { + this.applyBtnStyle(btn, value === decision); + } + const show = decision === "validated"; + ref.arrow.style.display = show ? "" : "none"; + ref.resLine.style.display = show ? "" : "none"; + const labels = { + validated: "", + ignored: "Conserv\xE9 tel quel dans ce fichier", + false_positive: "Faux positif \u2014 exclu du remplacement" + }; + ref.statusLabel.style.display = show ? "none" : ""; + ref.statusLabel.setText(labels[decision]); + } + // Met à jour TOUTES les cartes + updateAllCards() { + for (const occId of this.cardRefs.keys()) { + this.updateCard(occId); + } + } + applyCardStyle(card, decision) { + const borders = { + validated: "rgba(60,200,100,.7)", + ignored: "rgba(150,150,150,.4)", + false_positive: "rgba(255,80,80,.6)" + }; + const opacity = decision === "validated" ? "1" : "0.55"; + card.style.cssText = `border:1px solid var(--background-modifier-border);border-left:3px solid ${borders[decision]};border-radius:6px;padding:8px 10px;margin:4px 0;opacity:${opacity};`; + } + applyBtnStyle(btn, active) { + btn.style.cssText = `padding:2px 10px;border-radius:4px;cursor:pointer;font-size:.85em;border:1px solid var(--background-modifier-border);background:${active ? "var(--interactive-accent)" : "var(--background-primary)"};color:${active ? "var(--text-on-accent)" : "var(--text-normal)"};`; + } + ctxSpan(parent, text) { + const s = parent.createSpan({ text }); + s.style.opacity = "0.5"; + } + async apply() { + const validated = this.occurrences.filter((o) => this.decisions.get(o.id) === "validated"); + const ignored = this.occurrences.filter((o) => this.decisions.get(o.id) === "ignored"); + const spans = validated.map((occ) => { + var _a; + const rule = this.rules.find((r) => r.id === occ.mappingId); + return { start: occ.start, end: occ.end, source: occ.text, replacement: rule.replacement, mappingId: (_a = occ.mappingId) != null ? _a : "", priority: rule.priority }; + }); + const updated = applySpans(this.content, resolveSpans(spans)); + await this.app.vault.modify(this.file, updated); + await this.plugin.updateMappingStatuses(this.file.path, this.rules, this.occurrences, this.decisions); + const nv = validated.length, ni = ignored.length; + new import_obsidian5.Notice(`\u2713 ${nv} remplacement${nv > 1 ? "s" : ""} appliqu\xE9${nv > 1 ? "s" : ""}` + (ni > 0 ? `, ${ni} ignor\xE9${ni > 1 ? "s" : ""}` : "")); + this.plugin.refreshHighlightData(); + this.close(); + } + onClose() { + this.contentEl.empty(); + } +}; + +// src/pseudonymizer/ReplacementPlanner.ts +var DEFAULT_PLANNER_SETTINGS = { + caseSensitive: false, + wholeWordOnly: true +}; +function sortRules(rules) { + return [...rules].sort((a, b) => { + if (b.priority !== a.priority) + return b.priority - a.priority; + if (b.source.length !== a.source.length) + return b.source.length - a.source.length; + return scopeWeight(b.scope) - scopeWeight(a.scope); + }); +} +function scopeWeight(scope) { + if (scope.type === "file") + return 3; + if (scope.type === "folder") + return 2; + return 1; +} +var WORD_CHAR = /[\wÀ-ɏ]/; +function isWholeWord(text, start, end) { + if (start > 0 && WORD_CHAR.test(text[start - 1])) + return false; + if (end < text.length && WORD_CHAR.test(text[end])) + return false; + return true; +} +function findSpansForRule(text, rule, settings) { + const spans = []; + const needle = settings.caseSensitive ? rule.source : rule.source.toLowerCase(); + const haystack = settings.caseSensitive ? text : text.toLowerCase(); + const sourceLen = needle.length; + let pos = 0; + while (pos <= haystack.length - sourceLen) { + const idx = haystack.indexOf(needle, pos); + if (idx === -1) + break; + if (!settings.wholeWordOnly || isWholeWord(text, idx, idx + sourceLen)) { + spans.push({ + start: idx, + end: idx + sourceLen, + source: text.slice(idx, idx + sourceLen), + replacement: rule.replacement, + mappingId: rule.id, + priority: rule.priority + }); + } + pos = idx + 1; + } + return spans; +} +function buildReplacementPlan(text, rules, settings = DEFAULT_PLANNER_SETTINGS) { + const allSpans = []; + for (const rule of sortRules(rules)) { + allSpans.push(...findSpansForRule(text, rule, settings)); + } + return allSpans; +} + +// src/scanner/OccurrenceScanner.ts +var CONTEXT_LEN = 45; +var _counter = 0; +function lineOf(text, pos) { + return text.slice(0, pos).split("\n").length; +} +function context(text, start, end) { + return { + before: text.slice(Math.max(0, start - CONTEXT_LEN), start), + after: text.slice(end, Math.min(text.length, end + CONTEXT_LEN)) + }; +} +function scanOccurrences(content, filePath, rules, settings = DEFAULT_PLANNER_SETTINGS) { + const occurrences = []; + for (const rule of sortRules(rules)) { + for (const span of findSpansForRule(content, rule, settings)) { + const { before, after } = context(content, span.start, span.end); + occurrences.push({ + id: `occ_${Date.now()}_${++_counter}`, + file: filePath, + line: lineOf(content, span.start), + start: span.start, + end: span.end, + text: span.source, + contextBefore: before, + contextAfter: after, + category: rule.category, + mappingId: rule.id, + status: "suggested" + }); + } + } + return occurrences.sort((a, b) => a.start - b.start); +} + +// src/parsers/SrtParser.ts +var SrtParser = class { + parse(content) { + const trailingNewline = content.endsWith("\n"); + const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trimEnd(); + const rawBlocks = normalized.split(/\n\n+/).filter((b) => b.trim() !== ""); + const blocks = rawBlocks.map((rawBlock) => { + const lines = rawBlock.split("\n"); + const index = parseInt(lines[0].trim(), 10); + const arrowPos = lines[1].indexOf(" --> "); + const startTime = lines[1].slice(0, arrowPos); + const endTime = lines[1].slice(arrowPos + 5); + return { index, startTime, endTime, lines: lines.slice(2) }; + }); + return { blocks, trailingNewline }; + } + reconstruct(doc) { + const body = doc.blocks.map((b) => `${b.index} +${b.startTime} --> ${b.endTime} +${b.lines.join("\n")}`).join("\n\n"); + return doc.trailingNewline ? body + "\n" : body; + } +}; + +// src/parsers/ChatParser.ts +var ChatParser = class { + parse(content) { + const trailingNewline = content.endsWith("\n"); + const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + const rawLines = normalized.split("\n"); + const linesToParse = trailingNewline && rawLines[rawLines.length - 1] === "" ? rawLines.slice(0, -1) : rawLines; + return { lines: linesToParse.map((raw) => this.parseLine(raw)), trailingNewline }; + } + parseLine(raw) { + var _a, _b; + if (raw === "") + return { type: "blank", raw }; + if (raw.startsWith("@")) + return { type: "meta", raw }; + if (raw.startsWith("%")) + return { type: "dependent", raw }; + if (raw.startsWith(" ")) + return { type: "continuation", raw }; + if (raw.startsWith("*")) { + const colonIdx = raw.indexOf(":"); + if (colonIdx > 1) { + const speaker = raw.slice(1, colonIdx); + const afterColon = raw.slice(colonIdx + 1); + const sep = (_b = (_a = afterColon.match(/^([\t ]*)/)) == null ? void 0 : _a[1]) != null ? _b : ""; + const prefix = raw.slice(0, colonIdx + 1) + sep; + const content = afterColon.slice(sep.length); + return { type: "turn", raw, speaker, prefix, content }; + } + } + return { type: "meta", raw }; + } + reconstruct(doc) { + const body = doc.lines.map((line) => { + if (line.type === "turn" && line.prefix !== void 0 && line.content !== void 0) { + return line.prefix + line.content; + } + return line.raw; + }).join("\n"); + return doc.trailingNewline ? body + "\n" : body; + } +}; + +// src/parsers/TranscriptConverter.ts +function srtToMarkdown(doc, sourceName) { + const lines = [ + "---", + `pseudobs-format: srt`, + `pseudobs-source: "${sourceName}"`, + "---", + "" + ]; + for (const block of doc.blocks) { + lines.push(`**[${block.index}]** *${block.startTime} \u2192 ${block.endTime}*`); + lines.push(...block.lines); + lines.push(""); + } + while (lines[lines.length - 1] === "") + lines.pop(); + lines.push(""); + return lines.join("\n"); +} +function chatToMarkdown(doc, sourceName) { + const lines = [ + "---", + `pseudobs-format: chat`, + `pseudobs-source: "${sourceName}"`, + "---", + "" + ]; + let prevGroup = null; + for (const chatLine of doc.lines) { + const group = lineGroup(chatLine); + if (prevGroup !== null && prevGroup !== group) { + lines.push(""); + } + switch (chatLine.type) { + case "meta": + case "dependent": + lines.push(`> ${chatLine.raw}`); + break; + case "turn": + lines.push(`**${chatLine.speaker}** : ${chatLine.content}`); + break; + case "continuation": + if (lines.length > 0) { + lines[lines.length - 1] += ` ${chatLine.raw.trim()}`; + } + break; + case "blank": + break; + } + if (group !== null) + prevGroup = group; + } + while (lines[lines.length - 1] === "") + lines.pop(); + lines.push(""); + return lines.join("\n"); +} +function lineGroup(line) { + if (line.type === "meta" || line.type === "dependent") + return "structural"; + if (line.type === "turn" || line.type === "continuation") + return "turn"; + return null; +} + +// src/mappings/ScopeResolver.ts +var import_obsidian6 = require("obsidian"); +var ScopeResolver = class { + constructor(vault, mappingFolder) { + this.vault = vault; + this.mappingFolder = mappingFolder; + } + async getRulesFor(filePath) { + const folder = this.vault.getAbstractFileByPath(this.mappingFolder); + if (!(folder instanceof import_obsidian6.TFolder)) + return []; + const allRules = []; + for (const child of folder.children) { + if (!(child instanceof import_obsidian6.TFile)) + continue; + if (!child.name.endsWith(".mapping.json")) + continue; + try { + const raw = await this.vault.read(child); + const data = JSON.parse(raw); + const store = MappingStore.fromJSON(data); + allRules.push(...store.getValidatedFor(filePath)); + } catch (e) { + } + } + const seen = /* @__PURE__ */ new Set(); + return allRules.filter((r) => { + const key = `${r.source}||${r.replacement}||${r.scope.type}`; + if (seen.has(key)) + return false; + seen.add(key); + return true; + }); + } + // Retrouve la première règle dont la source OU le remplacement correspond au terme. + // Retourne aussi le store et le chemin JSON pour permettre la modification. + async findRuleByTerm(term) { + const folder = this.vault.getAbstractFileByPath(this.mappingFolder); + if (!(folder instanceof import_obsidian6.TFolder)) + return null; + const needle = term.toLowerCase(); + for (const child of folder.children) { + if (!(child instanceof import_obsidian6.TFile) || !child.name.endsWith(".mapping.json")) + continue; + try { + const data = JSON.parse(await this.vault.read(child)); + const store = MappingStore.fromJSON(data); + const rule = store.getAll().find( + (r) => r.source.toLowerCase() === needle || r.replacement.toLowerCase() === needle + ); + if (rule) + return { rule, store, filePath: child.path }; + } catch (e) { + } + } + return null; + } + // Sauvegarde un store modifié dans son fichier JSON. + async saveStore(store, filePath) { + const file = this.vault.getAbstractFileByPath(filePath); + const json = JSON.stringify(store.toJSON(), null, 2); + if (file instanceof import_obsidian6.TFile) { + await this.vault.modify(file, json); + } + } +}; + +// src/pseudonymizer/PseudonymizationEngine.ts +var PseudonymizationEngine = class { + constructor(settings = DEFAULT_PLANNER_SETTINGS) { + this.settings = settings; + } + // Applique toutes les règles validées sur le texte et retourne le texte pseudonymisé. + // Si marker est fourni, chaque remplacement est encadré par marker.open / marker.close. + pseudonymize(text, rules, marker) { + const validated = rules.filter((r) => r.status === "validated"); + const candidates = buildReplacementPlan(text, validated, this.settings); + const resolved = resolveSpans(candidates); + return applySpans(text, resolved, marker); + } +}; + +// src/main.ts +var CONVERTIBLE_EXTS = ["srt", "cha", "chat"]; +var PseudObsPlugin = class extends import_obsidian7.Plugin { + constructor() { + super(...arguments); + // Cache synchrone pour le surlignage CM6 (mis à jour de façon asynchrone) + this.highlightData = { sources: [], replacements: [] }; + } + async onload() { + await this.loadSettings(); + this.scopeResolver = new ScopeResolver(this.app.vault, this.settings.mappingFolder); + this.addSettingTab(new PseudObsSettingTab(this.app, this)); + this.registerEditorExtension( + createPseudonymHighlighter(() => this.highlightData) + ); + this.registerEvent( + this.app.workspace.on("active-leaf-change", () => this.refreshHighlightData()) + ); + this.refreshHighlightData(); + this.registerEvent( + this.app.vault.on("create", (file) => { + if (!(file instanceof import_obsidian7.TFile)) + return; + if (!CONVERTIBLE_EXTS.includes(file.extension.toLowerCase())) + return; + setTimeout(() => this.autoConvert(file), 300); + }) + ); + this.addCommand({ + id: "add-transcription", + name: "Ajouter une transcription", + callback: () => this.openFilePicker() + }); + this.addCommand({ + id: "pseudonymize-current-file", + name: "Pseudonymiser le fichier courant", + callback: () => this.pseudonymizeActiveFile() + }); + this.addCommand({ + id: "create-rule", + name: "Cr\xE9er une r\xE8gle de remplacement", + editorCallback: (editor) => { + new RuleModal(this.app, this, editor.getSelection()).open(); + } + }); + this.addCommand({ + id: "scan-current-file", + name: "Scanner le fichier courant", + callback: () => this.scanCurrentFile() + }); + this.addCommand({ + id: "pseudonymize-selection", + name: "Pseudonymiser la s\xE9lection", + editorCheckCallback: (checking, editor) => { + if (!editor.getSelection()) + return false; + if (!checking) + new QuickPseudonymizeModal(this.app, this, editor).open(); + return true; + } + }); + this.registerEvent( + this.app.workspace.on("editor-menu", (menu, editor) => { + const selection = editor.getSelection().trim(); + if (!selection) + return; + menu.addSeparator(); + const selLower = selection.toLowerCase(); + const isKnown = this.highlightData.sources.some((s) => s.toLowerCase() === selLower) || this.highlightData.replacements.some((r) => r.toLowerCase() === selLower); + if (isKnown) { + menu.addItem( + (item) => item.setTitle(`Modifier la r\xE8gle pour "${selection.slice(0, 25)}${selection.length > 25 ? "\u2026" : ""}"`).setIcon("settings").onClick(async () => { + const location = await this.scopeResolver.findRuleByTerm(selection); + if (location) { + new EditRuleModal(this.app, this, location).open(); + } else { + new import_obsidian7.Notice("R\xE8gle introuvable dans les mappings."); + } + }) + ); + } + menu.addItem( + (item) => item.setTitle(`Pseudonymiser "${selection.slice(0, 25)}${selection.length > 25 ? "\u2026" : ""}"`).setIcon("eye-off").onClick(() => new QuickPseudonymizeModal(this.app, this, editor).open()) + ); + menu.addItem( + (item) => item.setTitle(`Pseudonymiser avec Coulmont\u2026`).setIcon("book-user").onClick(async () => { + const notice = new import_obsidian7.Notice("Recherche sur coulmont.com\u2026", 0); + const suggestions = await this.fetchCoulmont(selection); + notice.hide(); + if (suggestions.length === 0) { + new import_obsidian7.Notice(`Aucun r\xE9sultat Coulmont pour "${selection}".`); + return; + } + new RuleModal(this.app, this, selection, "", suggestions).open(); + }) + ); + menu.addItem( + (item) => item.setTitle("Cr\xE9er une r\xE8gle de remplacement\u2026").setIcon("pencil").onClick(() => new RuleModal(this.app, this, selection).open()) + ); + }) + ); + } + onunload() { + } + // --- Coulmont --- + // Interroge l'outil de Baptiste Coulmont pour suggérer un prénom équivalent. + // Le prénom source est envoyé à coulmont.com — ne pas utiliser pour des données + // déjà sensibles (utiliser un prénom de substitution neutre si besoin). + // Retourne tous les prénoms équivalents proposés par l'outil Coulmont. + // Le jeu de données ne différencie pas M/F — l'utilisateur choisit dans la liste. + async fetchCoulmont(prenom) { + try { + const url = `https://coulmont.com/bac/results.php?search=${encodeURIComponent(prenom)}`; + const response = await (0, import_obsidian7.requestUrl)({ url, method: "GET" }); + const doc = new DOMParser().parseFromString(response.text, "text/html"); + const els = doc.querySelectorAll( + "#hero > div > div > div > div > p.mb-1.mb-md-1 > a" + ); + const names = Array.from(els).map((el) => { + var _a, _b; + return (_b = (_a = el.textContent) == null ? void 0 : _a.trim()) != null ? _b : ""; + }).filter((n) => n.length > 0); + return [...new Set(names)]; + } catch (e) { + return []; + } + } + // --- Surlignage --- + async refreshHighlightData() { + const file = this.app.workspace.getActiveFile(); + if (!file) { + this.highlightData = { sources: [], replacements: [] }; + } else { + try { + const rules = await this.scopeResolver.getRulesFor(file.path); + this.highlightData = { + sources: rules.map((r) => r.source).filter(Boolean), + replacements: rules.map((r) => r.replacement).filter(Boolean) + }; + } catch (e) { + this.highlightData = { sources: [], replacements: [] }; + } + } + const view = this.app.workspace.getActiveViewOfType(import_obsidian7.MarkdownView); + const cm = (view == null ? void 0 : view.editor) && view.editor.cm; + cm == null ? void 0 : cm.dispatch({ effects: highlightDataChanged.of(void 0) }); + } + // --- Conversion automatique --- + async autoConvert(file) { + var _a, _b; + try { + const raw = await this.app.vault.read(file); + const ext = file.extension.toLowerCase(); + const basename = file.basename; + const folder = (_b = (_a = file.parent) == null ? void 0 : _a.path) != null ? _b : ""; + const mdPath = folder ? `${folder}/${basename}.md` : `${basename}.md`; + let mdContent; + if (ext === "srt") { + mdContent = srtToMarkdown(new SrtParser().parse(raw), file.name); + } else { + mdContent = chatToMarkdown(new ChatParser().parse(raw), file.name); + } + if (this.app.vault.getAbstractFileByPath(mdPath) instanceof import_obsidian7.TFile) { + new import_obsidian7.Notice(`\u26A0 ${basename}.md existe d\xE9j\xE0 \u2014 conversion ignor\xE9e pour ${file.name}`); + return; + } + await this.app.vault.create(mdPath, mdContent); + const mappingPath = `${this.settings.mappingFolder}/${basename}.mapping.json`; + if (!this.app.vault.getAbstractFileByPath(mappingPath)) { + await this.ensureFolder(this.settings.mappingFolder); + const store = new MappingStore({ type: "file", path: mdPath }); + await this.app.vault.create(mappingPath, JSON.stringify(store.toJSON(), null, 2)); + } + await this.app.vault.delete(file); + const mdFile = this.app.vault.getAbstractFileByPath(mdPath); + if (mdFile instanceof import_obsidian7.TFile) { + await this.app.workspace.getLeaf().openFile(mdFile); + } + new import_obsidian7.Notice(`\u2713 ${file.name} \u2192 ${basename}.md`); + } catch (e) { + new import_obsidian7.Notice(`Erreur de conversion de ${file.name} : ${e.message}`); + } + } + // --- Commande "Ajouter une transcription" --- + openFilePicker() { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".srt,.cha,.chat,.txt,.md"; + input.multiple = true; + input.style.cssText = "position:fixed;top:-9999px;left:-9999px;opacity:0;"; + document.body.appendChild(input); + input.addEventListener("change", async () => { + var _a, _b, _c; + const files = Array.from((_a = input.files) != null ? _a : []); + input.remove(); + for (const file of files) { + if (CONVERTIBLE_EXTS.includes((_c = (_b = file.name.split(".").pop()) == null ? void 0 : _b.toLowerCase()) != null ? _c : "")) { + await this.copyToVault(file); + } else { + await this.copyToVault(file); + } + } + }); + input.click(); + } + async copyToVault(browserFile) { + const raw = await browserFile.text(); + const destFolder = this.settings.transcriptionsFolder; + await this.ensureFolder(destFolder); + const destPath = `${destFolder}/${browserFile.name}`; + if (this.app.vault.getAbstractFileByPath(destPath) instanceof import_obsidian7.TFile) { + new import_obsidian7.Notice(`Le fichier existe d\xE9j\xE0 dans le vault : ${browserFile.name}`); + return; + } + await this.app.vault.create(destPath, raw); + } + // --- Pseudonymisation --- + async pseudonymizeActiveFile() { + const file = this.app.workspace.getActiveFile(); + if (!file) { + new import_obsidian7.Notice("Aucun fichier actif."); + return; + } + const ext = file.extension.toLowerCase(); + if (!["srt", "cha", "chat", "md", "txt"].includes(ext)) { + new import_obsidian7.Notice(`Format non pris en charge : .${ext}`); + return; + } + const content = await this.app.vault.read(file); + const rules = await this.scopeResolver.getRulesFor(file.path); + if (rules.length === 0) { + new import_obsidian7.Notice( + `Aucune r\xE8gle valid\xE9e. +Cr\xE9ez des r\xE8gles via Ctrl+P \u2192 "Cr\xE9er une r\xE8gle". +Mapping attendu : ${this.settings.mappingFolder}/${file.basename}.mapping.json` + ); + return; + } + const marker = this.settings.useMarkerInExport ? { open: this.settings.markerOpen, close: this.settings.markerClose } : void 0; + const engine = new PseudonymizationEngine({ + caseSensitive: this.settings.caseSensitive, + wholeWordOnly: this.settings.wholeWordOnly + }); + let pseudonymized; + if (ext === "srt") { + const parser = new SrtParser(); + const doc = parser.parse(content); + for (const block of doc.blocks) { + block.lines = block.lines.map((l) => engine.pseudonymize(l, rules, marker)); + } + pseudonymized = parser.reconstruct(doc); + } else if (ext === "cha" || ext === "chat") { + const parser = new ChatParser(); + const doc = parser.parse(content); + for (const line of doc.lines) { + if (line.type === "turn" && line.content !== void 0) { + line.content = engine.pseudonymize(line.content, rules, marker); + } + } + pseudonymized = parser.reconstruct(doc); + } else { + pseudonymized = engine.pseudonymize(content, rules, marker); + } + await this.ensureFolder(this.settings.exportsFolder); + const outputPath = `${this.settings.exportsFolder}/${file.basename}.pseudonymized.${ext}`; + const existing = this.app.vault.getAbstractFileByPath(outputPath); + if (existing instanceof import_obsidian7.TFile) { + await this.app.vault.modify(existing, pseudonymized); + } else { + await this.app.vault.create(outputPath, pseudonymized); + } + new import_obsidian7.Notice(`\u2713 ${rules.length} r\xE8gle(s) appliqu\xE9e(s) +\u2192 ${outputPath}`); + } + async scanCurrentFile() { + const file = this.app.workspace.getActiveFile(); + if (!file) { + new import_obsidian7.Notice("Aucun fichier actif."); + return; + } + const rules = await this.scopeResolver.getRulesFor(file.path); + if (rules.length === 0) { + new import_obsidian7.Notice('Aucune r\xE8gle pour ce fichier.\nCr\xE9ez des r\xE8gles via Ctrl+P \u2192 "Cr\xE9er une r\xE8gle".'); + return; + } + const content = await this.app.vault.read(file); + const occurrences = scanOccurrences(content, file.path, rules, { + caseSensitive: this.settings.caseSensitive, + wholeWordOnly: this.settings.wholeWordOnly + }); + if (occurrences.length === 0) { + new import_obsidian7.Notice("Aucune occurrence trouv\xE9e pour les r\xE8gles actives."); + return; + } + new OccurrencesModal(this.app, this, file, content, occurrences, rules).open(); + } + // Appelé par OccurrencesModal après application — met à jour les statuts des règles. + async updateMappingStatuses(filePath, rules, occurrences, decisions) { + const folder = this.app.vault.getAbstractFileByPath(this.settings.mappingFolder); + if (!folder) + return; + for (const rule of rules) { + const ruleOccs = occurrences.filter((o) => o.mappingId === rule.id); + if (ruleOccs.length === 0) + continue; + const validated = ruleOccs.filter((o) => decisions.get(o.id) === "validated").length; + const ignored = ruleOccs.filter((o) => decisions.get(o.id) === "ignored").length; + const fp = ruleOccs.filter((o) => decisions.get(o.id) === "false_positive").length; + let newStatus; + if (validated > 0 && ignored + fp > 0) + newStatus = "partial"; + else if (validated === ruleOccs.length) + newStatus = "validated"; + else + newStatus = "ignored"; + const location = await this.scopeResolver.findRuleByTerm(rule.source); + if (location) { + location.store.update(rule.id, { status: newStatus }); + await this.scopeResolver.saveStore(location.store, location.filePath); + } + } + } + async applyRuleToFile(file, source, replacement) { + const content = await this.app.vault.read(file); + const fakeRule = { + id: "_quick", + source, + replacement, + category: "custom", + scope: { type: "file", path: file.path }, + status: "validated", + priority: 0, + createdBy: "user", + createdAt: (/* @__PURE__ */ new Date()).toISOString() + }; + const spans = findSpansForRule(content, fakeRule, { + caseSensitive: this.settings.caseSensitive, + wholeWordOnly: this.settings.wholeWordOnly + }); + if (spans.length === 0) + return 0; + spans.sort((a, b) => b.start - a.start); + await this.app.vault.modify(file, applySpans(content, spans)); + return spans.length; + } + // --- Utilitaires --- + async ensureFolder(folderPath) { + const parts = folderPath.split("/").filter(Boolean); + let current = ""; + for (const part of parts) { + current = current ? `${current}/${part}` : part; + if (!this.app.vault.getAbstractFileByPath(current)) { + await this.app.vault.createFolder(current); + } + } + } + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + async saveSettings() { + await this.saveData(this.settings); + } +}; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f479db5 --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "pseudobsidianization", + "name": "PseudObsidian-ization", + "version": "0.0.1", + "minAppVersion": "1.4.0", + "description": "Correction et pseudonymisation de transcriptions d'entretiens (Jefferson, ICOR, SRT, CHAT/CHA).", + "author": "Axelle Abbadie", + "authorUrl": "https://cv.hal.science/axelle-abbadie/", + "isDesktopOnly": true +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b8d0a0d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5698 @@ +{ + "name": "pseudobsidianization", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pseudobsidianization", + "version": "0.0.1", + "devDependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "builtin-modules": "^3.3.0", + "esbuild": "^0.20.0", + "eslint": "^8.57.0", + "jest": "^29.5.0", + "obsidian": "latest", + "prettier": "^3.0.0", + "ts-jest": "^29.4.9", + "tslib": "^2.6.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@codemirror/state": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/obsidian": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", + "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.4", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7f0fc00 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "pseudobsidianization", + "version": "0.0.1", + "description": "Plugin Obsidian de pseudonymisation de transcriptions", + "author": "Axelle Abbadie", + "homepage": "https://cv.hal.science/axelle-abbadie/", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc --noEmit --skipLibCheck && node esbuild.config.mjs production", + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint src --ext .ts" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "builtin-modules": "^3.3.0", + "esbuild": "^0.20.0", + "eslint": "^8.57.0", + "jest": "^29.5.0", + "obsidian": "latest", + "prettier": "^3.0.0", + "ts-jest": "^29.4.9", + "tslib": "^2.6.0", + "typescript": "^5.3.0" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..c0a4170 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,458 @@ +import { Plugin, Notice, TFile, TAbstractFile, Editor, Menu, MarkdownView, requestUrl } from 'obsidian'; +import { EditorView } from '@codemirror/view'; +import { PseudObsSettings, DEFAULT_SETTINGS, PseudObsSettingTab } from './settings'; +import { RuleModal } from './ui/RuleModal'; +import { QuickPseudonymizeModal } from './ui/QuickPseudonymizeModal'; +import { createPseudonymHighlighter, highlightDataChanged, type HighlightData } from './ui/PseudonymHighlighter'; +import { EditRuleModal } from './ui/EditRuleModal'; +import { OccurrencesModal } from './ui/OccurrencesModal'; +import { scanOccurrences } from './scanner/OccurrenceScanner'; +import { SrtParser } from './parsers/SrtParser'; +import { ChatParser } from './parsers/ChatParser'; +import { srtToMarkdown, chatToMarkdown } from './parsers/TranscriptConverter'; +import { MappingStore } from './mappings/MappingStore'; +import { ScopeResolver } from './mappings/ScopeResolver'; +import { PseudonymizationEngine } from './pseudonymizer/PseudonymizationEngine'; +import { findSpansForRule } from './pseudonymizer/ReplacementPlanner'; +import { applySpans } from './pseudonymizer/SpanProtector'; +import type { MappingFile, MappingRule, MappingStatus, Occurrence } from './types'; + +const CONVERTIBLE_EXTS = ['srt', 'cha', 'chat']; + +export default class PseudObsPlugin extends Plugin { + settings!: PseudObsSettings; + scopeResolver!: ScopeResolver; + // Cache synchrone pour le surlignage CM6 (mis à jour de façon asynchrone) + private highlightData: HighlightData = { sources: [], replacements: [] }; + + async onload(): Promise { + await this.loadSettings(); + this.scopeResolver = new ScopeResolver(this.app.vault, this.settings.mappingFolder); + this.addSettingTab(new PseudObsSettingTab(this.app, this)); + + // Extension CM6 : surlignage des termes sources (orange) et remplacements (vert) + this.registerEditorExtension( + createPseudonymHighlighter(() => this.highlightData) + ); + + // Rafraîchir le cache de surlignage à chaque changement de fichier actif + this.registerEvent( + this.app.workspace.on('active-leaf-change', () => this.refreshHighlightData()) + ); + // Premier chargement au démarrage + this.refreshHighlightData(); + + // Watcher : convertir automatiquement tout .srt/.cha/.chat ajouté au vault + // (drag-and-drop, copie externe, commande "Ajouter une transcription") + this.registerEvent( + this.app.vault.on('create', (file: TAbstractFile) => { + if (!(file instanceof TFile)) return; + if (!CONVERTIBLE_EXTS.includes(file.extension.toLowerCase())) return; + // Délai court pour laisser Obsidian finir l'écriture du fichier + setTimeout(() => this.autoConvert(file), 300); + }) + ); + + this.addCommand({ + id: 'add-transcription', + name: 'Ajouter une transcription', + callback: () => this.openFilePicker(), + }); + + this.addCommand({ + id: 'pseudonymize-current-file', + name: 'Pseudonymiser le fichier courant', + callback: () => this.pseudonymizeActiveFile(), + }); + + this.addCommand({ + id: 'create-rule', + name: 'Créer une règle de remplacement', + editorCallback: (editor) => { + new RuleModal(this.app, this, editor.getSelection()).open(); + }, + }); + + this.addCommand({ + id: 'scan-current-file', + name: 'Scanner le fichier courant', + callback: () => this.scanCurrentFile(), + }); + + this.addCommand({ + id: 'pseudonymize-selection', + name: 'Pseudonymiser la sélection', + editorCheckCallback: (checking, editor) => { + if (!editor.getSelection()) return false; + if (!checking) new QuickPseudonymizeModal(this.app, this, editor).open(); + return true; + }, + }); + + this.registerEvent( + this.app.workspace.on('editor-menu', (menu: Menu, editor: Editor) => { + const selection = editor.getSelection().trim(); + if (!selection) return; + menu.addSeparator(); + + const selLower = selection.toLowerCase(); + const isKnown = + this.highlightData.sources.some((s) => s.toLowerCase() === selLower) || + this.highlightData.replacements.some((r) => r.toLowerCase() === selLower); + + if (isKnown) { + // Terme connu : proposer la modification en priorité + menu.addItem((item) => + item + .setTitle(`Modifier la règle pour "${selection.slice(0, 25)}${selection.length > 25 ? '…' : ''}"`) + .setIcon('settings') + .onClick(async () => { + const location = await this.scopeResolver.findRuleByTerm(selection); + if (location) { + new EditRuleModal(this.app, this, location).open(); + } else { + new Notice('Règle introuvable dans les mappings.'); + } + }) + ); + } + + menu.addItem((item) => + item + .setTitle(`Pseudonymiser "${selection.slice(0, 25)}${selection.length > 25 ? '…' : ''}"`) + .setIcon('eye-off') + .onClick(() => new QuickPseudonymizeModal(this.app, this, editor).open()) + ); + + menu.addItem((item) => + item + .setTitle(`Pseudonymiser avec Coulmont…`) + .setIcon('book-user') + .onClick(async () => { + const notice = new Notice('Recherche sur coulmont.com…', 0); + const suggestions = await this.fetchCoulmont(selection); + notice.hide(); + if (suggestions.length === 0) { + new Notice(`Aucun résultat Coulmont pour "${selection}".`); + return; + } + new RuleModal(this.app, this, selection, '', suggestions).open(); + }) + ); + menu.addItem((item) => + item + .setTitle('Créer une règle de remplacement…') + .setIcon('pencil') + .onClick(() => new RuleModal(this.app, this, selection).open()) + ); + }) + ); + } + + onunload(): void {} + + // --- Coulmont --- + + // Interroge l'outil de Baptiste Coulmont pour suggérer un prénom équivalent. + // Le prénom source est envoyé à coulmont.com — ne pas utiliser pour des données + // déjà sensibles (utiliser un prénom de substitution neutre si besoin). + // Retourne tous les prénoms équivalents proposés par l'outil Coulmont. + // Le jeu de données ne différencie pas M/F — l'utilisateur choisit dans la liste. + async fetchCoulmont(prenom: string): Promise { + try { + const url = `https://coulmont.com/bac/results.php?search=${encodeURIComponent(prenom)}`; + const response = await requestUrl({ url, method: 'GET' }); + const doc = new DOMParser().parseFromString(response.text, 'text/html'); + + // Récupérer tous les liens dans les deux blocs de résultats (M et F) + const els = doc.querySelectorAll( + '#hero > div > div > div > div > p.mb-1.mb-md-1 > a' + ); + const names = Array.from(els) + .map((el) => el.textContent?.trim() ?? '') + .filter((n) => n.length > 0); + + // Dédoublonner en préservant l'ordre + return [...new Set(names)]; + } catch { + return []; + } + } + + // --- Surlignage --- + + async refreshHighlightData(): Promise { + const file = this.app.workspace.getActiveFile(); + if (!file) { + this.highlightData = { sources: [], replacements: [] }; + } else { + try { + const rules = await this.scopeResolver.getRulesFor(file.path); + this.highlightData = { + sources: rules.map((r) => r.source).filter(Boolean), + replacements: rules.map((r) => r.replacement).filter(Boolean), + }; + } catch { + this.highlightData = { sources: [], replacements: [] }; + } + } + + // Dispatcher le StateEffect sur l'éditeur actif pour déclencher + // la reconstruction des décorations CM6 (le ViewPlugin ne se déclenche + // pas sur un changement de données externe sans ce signal) + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + const cm = view?.editor && ((view.editor as unknown as { cm?: EditorView }).cm); + cm?.dispatch({ effects: highlightDataChanged.of(undefined) }); + } + + // --- Conversion automatique --- + + private async autoConvert(file: TFile): Promise { + try { + const raw = await this.app.vault.read(file); + const ext = file.extension.toLowerCase(); + const basename = file.basename; + const folder = file.parent?.path ?? ''; + const mdPath = folder ? `${folder}/${basename}.md` : `${basename}.md`; + + let mdContent: string; + if (ext === 'srt') { + mdContent = srtToMarkdown(new SrtParser().parse(raw), file.name); + } else { + mdContent = chatToMarkdown(new ChatParser().parse(raw), file.name); + } + + // Si un .md du même nom existe déjà, ne pas écraser + if (this.app.vault.getAbstractFileByPath(mdPath) instanceof TFile) { + new Notice(`⚠ ${basename}.md existe déjà — conversion ignorée pour ${file.name}`); + return; + } + + await this.app.vault.create(mdPath, mdContent); + + // Mapping JSON vide + const mappingPath = `${this.settings.mappingFolder}/${basename}.mapping.json`; + if (!this.app.vault.getAbstractFileByPath(mappingPath)) { + await this.ensureFolder(this.settings.mappingFolder); + const store = new MappingStore({ type: 'file', path: mdPath }); + await this.app.vault.create(mappingPath, JSON.stringify(store.toJSON(), null, 2)); + } + + // Supprimer le fichier source non-Markdown maintenant remplacé par le .md + await this.app.vault.delete(file); + + // Ouvrir le .md + const mdFile = this.app.vault.getAbstractFileByPath(mdPath); + if (mdFile instanceof TFile) { + await this.app.workspace.getLeaf().openFile(mdFile); + } + + new Notice(`✓ ${file.name} → ${basename}.md`); + } catch (e) { + new Notice(`Erreur de conversion de ${file.name} : ${(e as Error).message}`); + } + } + + // --- Commande "Ajouter une transcription" --- + + private openFilePicker(): void { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.srt,.cha,.chat,.txt,.md'; + input.multiple = true; + // Pas de display:none — bloque le change event dans certaines versions d'Electron + input.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;'; + document.body.appendChild(input); + + input.addEventListener('change', async () => { + const files = Array.from(input.files ?? []); + input.remove(); + + for (const file of files) { + if (CONVERTIBLE_EXTS.includes(file.name.split('.').pop()?.toLowerCase() ?? '')) { + // Copier dans le vault → le watcher s'occupe de la conversion + await this.copyToVault(file); + } else { + await this.copyToVault(file); + } + } + }); + + input.click(); + } + + private async copyToVault(browserFile: File): Promise { + const raw = await browserFile.text(); + const destFolder = this.settings.transcriptionsFolder; + await this.ensureFolder(destFolder); + const destPath = `${destFolder}/${browserFile.name}`; + + if (this.app.vault.getAbstractFileByPath(destPath) instanceof TFile) { + new Notice(`Le fichier existe déjà dans le vault : ${browserFile.name}`); + return; + } + + // Créer le fichier brut dans le vault — le watcher vault.on('create') prendra le relai + // pour les formats convertibles (.srt, .cha, .chat) + await this.app.vault.create(destPath, raw); + } + + // --- Pseudonymisation --- + + private async pseudonymizeActiveFile(): Promise { + const file = this.app.workspace.getActiveFile(); + if (!file) { new Notice('Aucun fichier actif.'); return; } + + const ext = file.extension.toLowerCase(); + if (!['srt', 'cha', 'chat', 'md', 'txt'].includes(ext)) { + new Notice(`Format non pris en charge : .${ext}`); + return; + } + + const content = await this.app.vault.read(file); + // Charger les règles depuis les trois niveaux (fichier + dossier + vault) + const rules = await this.scopeResolver.getRulesFor(file.path); + if (rules.length === 0) { + new Notice( + `Aucune règle validée.\nCréez des règles via Ctrl+P → "Créer une règle".\nMapping attendu : ${this.settings.mappingFolder}/${file.basename}.mapping.json` + ); + return; + } + + const marker = this.settings.useMarkerInExport + ? { open: this.settings.markerOpen, close: this.settings.markerClose } + : undefined; + + const engine = new PseudonymizationEngine({ + caseSensitive: this.settings.caseSensitive, + wholeWordOnly: this.settings.wholeWordOnly, + }); + + let pseudonymized: string; + if (ext === 'srt') { + const parser = new SrtParser(); + const doc = parser.parse(content); + for (const block of doc.blocks) { + block.lines = block.lines.map((l) => engine.pseudonymize(l, rules, marker)); + } + pseudonymized = parser.reconstruct(doc); + } else if (ext === 'cha' || ext === 'chat') { + const parser = new ChatParser(); + const doc = parser.parse(content); + for (const line of doc.lines) { + if (line.type === 'turn' && line.content !== undefined) { + line.content = engine.pseudonymize(line.content, rules, marker); + } + } + pseudonymized = parser.reconstruct(doc); + } else { + pseudonymized = engine.pseudonymize(content, rules, marker); + } + + await this.ensureFolder(this.settings.exportsFolder); + const outputPath = `${this.settings.exportsFolder}/${file.basename}.pseudonymized.${ext}`; + const existing = this.app.vault.getAbstractFileByPath(outputPath); + if (existing instanceof TFile) { + await this.app.vault.modify(existing, pseudonymized); + } else { + await this.app.vault.create(outputPath, pseudonymized); + } + + new Notice(`✓ ${rules.length} règle(s) appliquée(s)\n→ ${outputPath}`); + } + + private async scanCurrentFile(): Promise { + const file = this.app.workspace.getActiveFile(); + if (!file) { new Notice('Aucun fichier actif.'); return; } + + const rules = await this.scopeResolver.getRulesFor(file.path); + if (rules.length === 0) { + new Notice('Aucune règle pour ce fichier.\nCréez des règles via Ctrl+P → "Créer une règle".'); + return; + } + + const content = await this.app.vault.read(file); + const occurrences = scanOccurrences(content, file.path, rules, { + caseSensitive: this.settings.caseSensitive, + wholeWordOnly: this.settings.wholeWordOnly, + }); + + if (occurrences.length === 0) { + new Notice('Aucune occurrence trouvée pour les règles actives.'); + return; + } + + new OccurrencesModal(this.app, this, file, content, occurrences, rules).open(); + } + + // Appelé par OccurrencesModal après application — met à jour les statuts des règles. + async updateMappingStatuses( + filePath: string, + rules: MappingRule[], + occurrences: Occurrence[], + decisions: Map + ): Promise { + const folder = this.app.vault.getAbstractFileByPath(this.settings.mappingFolder); + if (!folder) return; + + // Pour chaque règle concernée, calculer le statut global + for (const rule of rules) { + const ruleOccs = occurrences.filter((o) => o.mappingId === rule.id); + if (ruleOccs.length === 0) continue; + + const validated = ruleOccs.filter((o) => decisions.get(o.id) === 'validated').length; + const ignored = ruleOccs.filter((o) => decisions.get(o.id) === 'ignored').length; + const fp = ruleOccs.filter((o) => decisions.get(o.id) === 'false_positive').length; + + let newStatus: MappingStatus; + if (validated > 0 && (ignored + fp) > 0) newStatus = 'partial'; + else if (validated === ruleOccs.length) newStatus = 'validated'; + else newStatus = 'ignored'; + + // Trouver et mettre à jour dans le mapping JSON + const location = await this.scopeResolver.findRuleByTerm(rule.source); + if (location) { + location.store.update(rule.id, { status: newStatus }); + await this.scopeResolver.saveStore(location.store, location.filePath); + } + } + } + + async applyRuleToFile(file: TFile, source: string, replacement: string): Promise { + const content = await this.app.vault.read(file); + const fakeRule: MappingRule = { + id: '_quick', source, replacement, category: 'custom', + scope: { type: 'file', path: file.path }, status: 'validated', + priority: 0, createdBy: 'user', createdAt: new Date().toISOString(), + }; + const spans = findSpansForRule(content, fakeRule, { + caseSensitive: this.settings.caseSensitive, + wholeWordOnly: this.settings.wholeWordOnly, + }); + if (spans.length === 0) return 0; + spans.sort((a, b) => b.start - a.start); + await this.app.vault.modify(file, applySpans(content, spans)); + return spans.length; + } + + // --- Utilitaires --- + + async ensureFolder(folderPath: string): Promise { + const parts = folderPath.split('/').filter(Boolean); + let current = ''; + for (const part of parts) { + current = current ? `${current}/${part}` : part; + if (!this.app.vault.getAbstractFileByPath(current)) { + await this.app.vault.createFolder(current); + } + } + } + + async loadSettings(): Promise { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings(): Promise { + await this.saveData(this.settings); + } +} diff --git a/src/mappings/MappingStore.ts b/src/mappings/MappingStore.ts new file mode 100644 index 0000000..235fc98 --- /dev/null +++ b/src/mappings/MappingStore.ts @@ -0,0 +1,102 @@ +import type { MappingRule, MappingFile, MappingFileSettings, Scope } from '../types'; + +function generateId(): string { + return `map_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`; +} + +export const DEFAULT_FILE_SETTINGS: MappingFileSettings = { + caseSensitive: false, + accentSensitive: false, + wholeWordOnly: true, + preserveCase: true, + preserveGender: true, + preserveAnalyticNotation: true, +}; + +export class MappingStore { + private rules: Map = new Map(); + private fileSettings: MappingFileSettings; + readonly scope: Scope; + readonly project?: string; + readonly createdAt: string; + private updatedAt: string; + + constructor(scope: Scope, project?: string, settings?: Partial) { + this.scope = scope; + this.project = project; + this.createdAt = new Date().toISOString(); + this.updatedAt = this.createdAt; + this.fileSettings = { ...DEFAULT_FILE_SETTINGS, ...settings }; + } + + add(partial: Omit): MappingRule { + const rule: MappingRule = { + ...partial, + id: generateId(), + createdAt: new Date().toISOString(), + }; + this.rules.set(rule.id, rule); + this.touch(); + return rule; + } + + get(id: string): MappingRule | undefined { + return this.rules.get(id); + } + + update(id: string, changes: Partial>): boolean { + const rule = this.rules.get(id); + if (!rule) return false; + this.rules.set(id, { ...rule, ...changes, updatedAt: new Date().toISOString() }); + this.touch(); + return true; + } + + remove(id: string): boolean { + const deleted = this.rules.delete(id); + if (deleted) this.touch(); + return deleted; + } + + getAll(): MappingRule[] { + return Array.from(this.rules.values()); + } + + // Règles validées applicables à un fichier donné (cascade file → folder → vault) + getValidatedFor(filePath: string): MappingRule[] { + return this.getAll().filter((r) => { + if (r.status !== 'validated') return false; + if (r.scope.type === 'vault') return true; + if (r.scope.type === 'folder') return filePath.startsWith(r.scope.path ?? ''); + return r.scope.path === filePath; + }); + } + + get settings(): MappingFileSettings { + return this.fileSettings; + } + + toJSON(): MappingFile { + return { + schemaVersion: '1.0.0', + createdAt: this.createdAt, + updatedAt: this.updatedAt, + project: this.project, + scope: this.scope, + settings: this.fileSettings, + mappings: this.getAll(), + }; + } + + static fromJSON(data: MappingFile): MappingStore { + const store = new MappingStore(data.scope, data.project, data.settings); + for (const rule of data.mappings) { + store.rules.set(rule.id, rule); + } + return store; + } + + private touch(): void { + this.updatedAt = new Date().toISOString(); + } +} diff --git a/src/mappings/ScopeResolver.ts b/src/mappings/ScopeResolver.ts new file mode 100644 index 0000000..204b01b --- /dev/null +++ b/src/mappings/ScopeResolver.ts @@ -0,0 +1,85 @@ +import { TFile, TFolder, Vault } from 'obsidian'; +import { MappingStore } from './MappingStore'; +import type { MappingFile, MappingRule } from '../types'; + +export interface RuleLocation { + rule: MappingRule; + store: MappingStore; + filePath: string; // chemin du .mapping.json qui contient la règle +} + +// Parcourt le dossier de mappings, charge tous les fichiers JSON valides +// et retourne les règles validées applicables au fichier demandé. +// La cascade file → folder → vault est gérée par le tri sortRules (scopeWeight). +export class ScopeResolver { + constructor( + private vault: Vault, + private mappingFolder: string + ) {} + + async getRulesFor(filePath: string): Promise { + const folder = this.vault.getAbstractFileByPath(this.mappingFolder); + if (!(folder instanceof TFolder)) return []; + + const allRules: MappingRule[] = []; + + for (const child of folder.children) { + if (!(child instanceof TFile)) continue; + if (!child.name.endsWith('.mapping.json')) continue; + + try { + const raw = await this.vault.read(child); + const data: MappingFile = JSON.parse(raw); + const store = MappingStore.fromJSON(data); + allRules.push(...store.getValidatedFor(filePath)); + } catch { + // Ignorer silencieusement les fichiers de mapping malformés + } + } + + // Dédoublonner par (source, replacement) — la même règle peut apparaître + // dans plusieurs fichiers si elle a été copiée à plusieurs niveaux + const seen = new Set(); + return allRules.filter((r) => { + const key = `${r.source}||${r.replacement}||${r.scope.type}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + } + + // Retrouve la première règle dont la source OU le remplacement correspond au terme. + // Retourne aussi le store et le chemin JSON pour permettre la modification. + async findRuleByTerm(term: string): Promise { + const folder = this.vault.getAbstractFileByPath(this.mappingFolder); + if (!(folder instanceof TFolder)) return null; + + const needle = term.toLowerCase(); + + for (const child of folder.children) { + if (!(child instanceof TFile) || !child.name.endsWith('.mapping.json')) continue; + try { + const data: MappingFile = JSON.parse(await this.vault.read(child)); + const store = MappingStore.fromJSON(data); + const rule = store.getAll().find( + (r) => + r.source.toLowerCase() === needle || + r.replacement.toLowerCase() === needle + ); + if (rule) return { rule, store, filePath: child.path }; + } catch { + // ignorer les fichiers malformés + } + } + return null; + } + + // Sauvegarde un store modifié dans son fichier JSON. + async saveStore(store: MappingStore, filePath: string): Promise { + const file = this.vault.getAbstractFileByPath(filePath); + const json = JSON.stringify(store.toJSON(), null, 2); + if (file instanceof TFile) { + await this.vault.modify(file, json); + } + } +} diff --git a/src/parsers/BaseTranscriptParser.ts b/src/parsers/BaseTranscriptParser.ts new file mode 100644 index 0000000..5665c48 --- /dev/null +++ b/src/parsers/BaseTranscriptParser.ts @@ -0,0 +1,8 @@ +// Interface commune à tous les parsers de transcription (SPECS §11.2) + +export interface BaseTranscriptParser { + // Transforme le contenu brut en document structuré + parse(content: string): TDocument; + // Reconstruit le texte original depuis le document — doit être un round-trip exact + reconstruct(doc: TDocument): string; +} diff --git a/src/parsers/ChatParser.ts b/src/parsers/ChatParser.ts new file mode 100644 index 0000000..d131561 --- /dev/null +++ b/src/parsers/ChatParser.ts @@ -0,0 +1,76 @@ +import type { BaseTranscriptParser } from './BaseTranscriptParser'; + +// Types de lignes du format CHAT / CHA (SPECS §3.3) +export type ChatLineType = 'meta' | 'turn' | 'dependent' | 'continuation' | 'blank'; + +export interface ChatLine { + type: ChatLineType; + raw: string; // ligne originale complète — garantit le round-trip + // Présents uniquement pour type === 'turn' + speaker?: string; // identifiant du locuteur (ex. 'INV', 'PAR') + prefix?: string; // "*SPEAKER: " — tout ce qui précède le contenu remplaçable + content?: string; // texte du tour de parole (seule zone remplaçable) +} + +export interface ChatDocument { + lines: ChatLine[]; + trailingNewline: boolean; +} + +export class ChatParser implements BaseTranscriptParser { + parse(content: string): ChatDocument { + const trailingNewline = content.endsWith('\n'); + const normalized = content + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n'); + + const rawLines = normalized.split('\n'); + + // La dernière entrée après split est vide si le fichier finit par \n + // On la retire pour ne pas créer une ligne 'blank' fantôme + const linesToParse = + trailingNewline && rawLines[rawLines.length - 1] === '' + ? rawLines.slice(0, -1) + : rawLines; + + return { lines: linesToParse.map((raw) => this.parseLine(raw)), trailingNewline }; + } + + private parseLine(raw: string): ChatLine { + if (raw === '') return { type: 'blank', raw }; + if (raw.startsWith('@')) return { type: 'meta', raw }; + if (raw.startsWith('%')) return { type: 'dependent', raw }; + if (raw.startsWith('\t')) return { type: 'continuation', raw }; + + if (raw.startsWith('*')) { + const colonIdx = raw.indexOf(':'); + // colonIdx > 1 : il faut au moins un caractère de locuteur entre * et : + if (colonIdx > 1) { + const speaker = raw.slice(1, colonIdx); + const afterColon = raw.slice(colonIdx + 1); + // préserver le séparateur exact (espace, tabulation, ou les deux) + const sep = afterColon.match(/^([\t ]*)/)?.[1] ?? ''; + const prefix = raw.slice(0, colonIdx + 1) + sep; + const content = afterColon.slice(sep.length); + return { type: 'turn', raw, speaker, prefix, content }; + } + } + + // Ligne non reconnue → préserver telle quelle + return { type: 'meta', raw }; + } + + reconstruct(doc: ChatDocument): string { + const body = doc.lines + .map((line) => { + // Reconstruire les tours depuis prefix + content (éventuellement modifié) + if (line.type === 'turn' && line.prefix !== undefined && line.content !== undefined) { + return line.prefix + line.content; + } + return line.raw; + }) + .join('\n'); + + return doc.trailingNewline ? body + '\n' : body; + } +} diff --git a/src/parsers/SrtParser.ts b/src/parsers/SrtParser.ts new file mode 100644 index 0000000..add9b7b --- /dev/null +++ b/src/parsers/SrtParser.ts @@ -0,0 +1,49 @@ +import type { BaseTranscriptParser } from './BaseTranscriptParser'; + +export interface SrtBlock { + index: number; + startTime: string; + endTime: string; + lines: string[]; // lignes de texte remplaçables uniquement +} + +export interface SrtDocument { + blocks: SrtBlock[]; + // préservé pour le round-trip exact + trailingNewline: boolean; +} + +export class SrtParser implements BaseTranscriptParser { + parse(content: string): SrtDocument { + const trailingNewline = content.endsWith('\n'); + // normaliser les fins de ligne et retirer le blanc final avant de découper en blocs + // (sinon le dernier bloc capture le \n terminal dans ses lines[]) + const normalized = content + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .trimEnd(); + + const rawBlocks = normalized.split(/\n\n+/).filter((b) => b.trim() !== ''); + + const blocks: SrtBlock[] = rawBlocks.map((rawBlock) => { + const lines = rawBlock.split('\n'); + const index = parseInt(lines[0].trim(), 10); + + // "HH:MM:SS,mmm --> HH:MM:SS,mmm" + const arrowPos = lines[1].indexOf(' --> '); + const startTime = lines[1].slice(0, arrowPos); + const endTime = lines[1].slice(arrowPos + 5); + + return { index, startTime, endTime, lines: lines.slice(2) }; + }); + + return { blocks, trailingNewline }; + } + + reconstruct(doc: SrtDocument): string { + const body = doc.blocks + .map((b) => `${b.index}\n${b.startTime} --> ${b.endTime}\n${b.lines.join('\n')}`) + .join('\n\n'); + return doc.trailingNewline ? body + '\n' : body; + } +} diff --git a/src/parsers/TranscriptConverter.ts b/src/parsers/TranscriptConverter.ts new file mode 100644 index 0000000..6e8641f --- /dev/null +++ b/src/parsers/TranscriptConverter.ts @@ -0,0 +1,83 @@ +import type { SrtDocument } from './SrtParser'; +import type { ChatDocument, ChatLine } from './ChatParser'; + +// Converts a SrtDocument to a structured, Obsidian-readable Markdown string. +// Timestamps are preserved as italic headers; text lines are the editable content. +// Frontmatter records the original format for future re-export. +export function srtToMarkdown(doc: SrtDocument, sourceName: string): string { + const lines: string[] = [ + '---', + `pseudobs-format: srt`, + `pseudobs-source: "${sourceName}"`, + '---', + '', + ]; + + for (const block of doc.blocks) { + lines.push(`**[${block.index}]** *${block.startTime} → ${block.endTime}*`); + lines.push(...block.lines); + lines.push(''); + } + + // Retirer la dernière ligne vide superflue, puis ajouter \n final + while (lines[lines.length - 1] === '') lines.pop(); + lines.push(''); + + return lines.join('\n'); +} + +// Converts a ChatDocument to a structured, Obsidian-readable Markdown string. +// Meta (@) and dependent (%) lines become Markdown blockquotes (gray in preview). +// Speaker turns become **SPEAKER** : text (bold speaker name, editable content). +export function chatToMarkdown(doc: ChatDocument, sourceName: string): string { + const lines: string[] = [ + '---', + `pseudobs-format: chat`, + `pseudobs-source: "${sourceName}"`, + '---', + '', + ]; + + let prevGroup: 'structural' | 'turn' | null = null; + + for (const chatLine of doc.lines) { + const group = lineGroup(chatLine); + + // Ligne vide entre groupes structurels et tours de parole + if (prevGroup !== null && prevGroup !== group) { + lines.push(''); + } + + switch (chatLine.type) { + case 'meta': + case 'dependent': + lines.push(`> ${chatLine.raw}`); + break; + case 'turn': + lines.push(`**${chatLine.speaker}** : ${chatLine.content}`); + break; + case 'continuation': + // Rattacher au tour précédent (rare en pratique) + if (lines.length > 0) { + lines[lines.length - 1] += ` ${chatLine.raw.trim()}`; + } + break; + case 'blank': + // Les blancs du source sont absorbés par la logique de groupe ci-dessus + break; + } + + if (group !== null) prevGroup = group; + } + + while (lines[lines.length - 1] === '') lines.pop(); + lines.push(''); + + return lines.join('\n'); +} + +function lineGroup(line: ChatLine): 'structural' | 'turn' | null { + if (line.type === 'meta' || line.type === 'dependent') return 'structural'; + if (line.type === 'turn' || line.type === 'continuation') return 'turn'; + return null; +} diff --git a/src/pseudonymizer/PseudonymizationEngine.ts b/src/pseudonymizer/PseudonymizationEngine.ts new file mode 100644 index 0000000..c380afc --- /dev/null +++ b/src/pseudonymizer/PseudonymizationEngine.ts @@ -0,0 +1,24 @@ +import type { MappingRule } from '../types'; +import { + buildReplacementPlan, + DEFAULT_PLANNER_SETTINGS, + type PlannerSettings, +} from './ReplacementPlanner'; +import { resolveSpans, applySpans, type ReplacementMarker } from './SpanProtector'; + +export class PseudonymizationEngine { + private settings: PlannerSettings; + + constructor(settings: PlannerSettings = DEFAULT_PLANNER_SETTINGS) { + this.settings = settings; + } + + // Applique toutes les règles validées sur le texte et retourne le texte pseudonymisé. + // Si marker est fourni, chaque remplacement est encadré par marker.open / marker.close. + pseudonymize(text: string, rules: MappingRule[], marker?: ReplacementMarker): string { + const validated = rules.filter((r) => r.status === 'validated'); + const candidates = buildReplacementPlan(text, validated, this.settings); + const resolved = resolveSpans(candidates); + return applySpans(text, resolved, marker); + } +} diff --git a/src/pseudonymizer/ReplacementPlanner.ts b/src/pseudonymizer/ReplacementPlanner.ts new file mode 100644 index 0000000..9c7d3d4 --- /dev/null +++ b/src/pseudonymizer/ReplacementPlanner.ts @@ -0,0 +1,81 @@ +import type { MappingRule, ReplacementSpan, Scope } from '../types'; + +export interface PlannerSettings { + caseSensitive: boolean; + wholeWordOnly: boolean; +} + +export const DEFAULT_PLANNER_SETTINGS: PlannerSettings = { + caseSensitive: false, + wholeWordOnly: true, +}; + +// Tri des règles : priority desc → longueur source desc → portée locale desc (SPECS §12.3) +export function sortRules(rules: MappingRule[]): MappingRule[] { + return [...rules].sort((a, b) => { + if (b.priority !== a.priority) return b.priority - a.priority; + if (b.source.length !== a.source.length) return b.source.length - a.source.length; + return scopeWeight(b.scope) - scopeWeight(a.scope); + }); +} + +function scopeWeight(scope: Scope): number { + if (scope.type === 'file') return 3; + if (scope.type === 'folder') return 2; + return 1; +} + +// Inclut les lettres latines accentuées pour ne pas couper les mots français +const WORD_CHAR = /[\wÀ-ɏ]/; + +function isWholeWord(text: string, start: number, end: number): boolean { + if (start > 0 && WORD_CHAR.test(text[start - 1])) return false; + if (end < text.length && WORD_CHAR.test(text[end])) return false; + return true; +} + +// Trouve toutes les occurrences d'une règle dans le texte +export function findSpansForRule( + text: string, + rule: MappingRule, + settings: PlannerSettings +): ReplacementSpan[] { + const spans: ReplacementSpan[] = []; + const needle = settings.caseSensitive ? rule.source : rule.source.toLowerCase(); + const haystack = settings.caseSensitive ? text : text.toLowerCase(); + const sourceLen = needle.length; + + let pos = 0; + while (pos <= haystack.length - sourceLen) { + const idx = haystack.indexOf(needle, pos); + if (idx === -1) break; + + if (!settings.wholeWordOnly || isWholeWord(text, idx, idx + sourceLen)) { + spans.push({ + start: idx, + end: idx + sourceLen, + source: text.slice(idx, idx + sourceLen), + replacement: rule.replacement, + mappingId: rule.id, + priority: rule.priority, + }); + } + + pos = idx + 1; + } + + return spans; +} + +// Construit le plan de remplacement complet pour un texte (SPECS §12.2) +export function buildReplacementPlan( + text: string, + rules: MappingRule[], + settings: PlannerSettings = DEFAULT_PLANNER_SETTINGS +): ReplacementSpan[] { + const allSpans: ReplacementSpan[] = []; + for (const rule of sortRules(rules)) { + allSpans.push(...findSpansForRule(text, rule, settings)); + } + return allSpans; +} diff --git a/src/pseudonymizer/SpanProtector.ts b/src/pseudonymizer/SpanProtector.ts new file mode 100644 index 0000000..af97ef6 --- /dev/null +++ b/src/pseudonymizer/SpanProtector.ts @@ -0,0 +1,46 @@ +import type { ReplacementSpan } from '../types'; + +// Résout les chevauchements : garde les spans les plus prioritaires / les plus longs, +// rejette ceux qui chevauchent un span déjà accepté. (SPECS §12.4) +export function resolveSpans(candidates: ReplacementSpan[]): ReplacementSpan[] { + const sorted = [...candidates].sort((a, b) => { + if (b.priority !== a.priority) return b.priority - a.priority; + const lenB = b.end - b.start; + const lenA = a.end - a.start; + if (lenB !== lenA) return lenB - lenA; + return a.start - b.start; // à priorité et longueur égales : position croissante + }); + + const accepted: ReplacementSpan[] = []; + for (const candidate of sorted) { + const overlaps = accepted.some( + (s) => candidate.start < s.end && candidate.end > s.start + ); + if (!overlaps) accepted.push(candidate); + } + + // Trier de droite à gauche pour que l'application ne décale pas les indices + return accepted.sort((a, b) => b.start - a.start); +} + +export interface ReplacementMarker { + open: string; + close: string; +} + +// Applique les spans résolus sur le texte de droite à gauche. (SPECS §12.5) +// Si marker est fourni, chaque remplacement est encadré par marker.open / marker.close. +export function applySpans( + text: string, + spans: ReplacementSpan[], + marker?: ReplacementMarker +): string { + let output = text; + for (const span of spans) { + const value = marker + ? `${marker.open}${span.replacement}${marker.close}` + : span.replacement; + output = output.slice(0, span.start) + value + output.slice(span.end); + } + return output; +} diff --git a/src/scanner/OccurrenceScanner.ts b/src/scanner/OccurrenceScanner.ts new file mode 100644 index 0000000..bbb9657 --- /dev/null +++ b/src/scanner/OccurrenceScanner.ts @@ -0,0 +1,49 @@ +import type { MappingRule, Occurrence } from '../types'; +import { findSpansForRule, sortRules, type PlannerSettings } from '../pseudonymizer/ReplacementPlanner'; +import { DEFAULT_PLANNER_SETTINGS } from '../pseudonymizer/ReplacementPlanner'; + +const CONTEXT_LEN = 45; +let _counter = 0; + +function lineOf(text: string, pos: number): number { + return text.slice(0, pos).split('\n').length; +} + +function context(text: string, start: number, end: number) { + return { + before: text.slice(Math.max(0, start - CONTEXT_LEN), start), + after: text.slice(end, Math.min(text.length, end + CONTEXT_LEN)), + }; +} + +// Retourne toutes les occurrences des sources des règles dans le contenu, +// triées par position, avec contexte gauche/droite et numéro de ligne. +export function scanOccurrences( + content: string, + filePath: string, + rules: MappingRule[], + settings: PlannerSettings = DEFAULT_PLANNER_SETTINGS +): Occurrence[] { + const occurrences: Occurrence[] = []; + + for (const rule of sortRules(rules)) { + for (const span of findSpansForRule(content, rule, settings)) { + const { before, after } = context(content, span.start, span.end); + occurrences.push({ + id: `occ_${Date.now()}_${++_counter}`, + file: filePath, + line: lineOf(content, span.start), + start: span.start, + end: span.end, + text: span.source, + contextBefore: before, + contextAfter: after, + category: rule.category, + mappingId: rule.id, + status: 'suggested', + }); + } + } + + return occurrences.sort((a, b) => a.start - b.start); +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..4d6b493 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,187 @@ +import { App, PluginSettingTab, Setting } from 'obsidian'; +import type PseudObsPlugin from './main'; + +export interface PseudObsSettings { + transcriptionsFolder: string; + mappingFolder: string; + dictionariesFolder: string; + exportsFolder: string; + reportsFolder: string; + caseSensitive: boolean; + accentSensitive: boolean; + wholeWordOnly: boolean; + preserveCase: boolean; + preserveAnalyticNotation: boolean; + warnIfSyncedFolder: boolean; + useMarkerInExport: boolean; + markerOpen: string; + markerClose: string; +} + +export const DEFAULT_SETTINGS: PseudObsSettings = { + transcriptionsFolder: 'Transcriptions', + mappingFolder: '_pseudonymisation/mappings', + dictionariesFolder: '_pseudonymisation/dictionaries', + exportsFolder: '_pseudonymisation/exports', + reportsFolder: '_pseudonymisation/reports', + caseSensitive: false, + accentSensitive: false, + wholeWordOnly: true, + preserveCase: true, + preserveAnalyticNotation: true, + warnIfSyncedFolder: true, + useMarkerInExport: false, + markerOpen: '⟦', + markerClose: '⟧', +}; + +export class PseudObsSettingTab extends PluginSettingTab { + plugin: PseudObsPlugin; + + constructor(app: App, plugin: PseudObsPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl('h2', { text: 'PseudObsidian-ization' }); + + containerEl.createEl('h3', { text: 'Dossiers' }); + + new Setting(containerEl) + .setName('Transcriptions importées') + .setDesc('Dossier de destination pour les fichiers ajoutés via la commande "Ajouter une transcription"') + .addText((text) => + text.setValue(this.plugin.settings.transcriptionsFolder).onChange(async (value) => { + this.plugin.settings.transcriptionsFolder = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Tables de correspondance') + .setDesc('Chemin relatif dans le vault') + .addText((text) => + text.setValue(this.plugin.settings.mappingFolder).onChange(async (value) => { + this.plugin.settings.mappingFolder = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Dictionnaires') + .setDesc('Chemin relatif dans le vault') + .addText((text) => + text.setValue(this.plugin.settings.dictionariesFolder).onChange(async (value) => { + this.plugin.settings.dictionariesFolder = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Exports') + .addText((text) => + text.setValue(this.plugin.settings.exportsFolder).onChange(async (value) => { + this.plugin.settings.exportsFolder = value; + await this.plugin.saveSettings(); + }) + ); + + containerEl.createEl('h3', { text: 'Remplacement' }); + + new Setting(containerEl) + .setName('Sensible à la casse') + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.caseSensitive).onChange(async (value) => { + this.plugin.settings.caseSensitive = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Sensible aux accents') + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.accentSensitive).onChange(async (value) => { + this.plugin.settings.accentSensitive = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Mots entiers uniquement') + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.wholeWordOnly).onChange(async (value) => { + this.plugin.settings.wholeWordOnly = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Préserver la casse') + .setDesc('Adapter la casse du remplacement à celle de la source (Jean → Pierre, JEAN → PIERRE)') + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.preserveCase).onChange(async (value) => { + this.plugin.settings.preserveCase = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Préserver les notations analytiques') + .setDesc('Ne jamais remplacer les symboles Jefferson / ICOR') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.preserveAnalyticNotation) + .onChange(async (value) => { + this.plugin.settings.preserveAnalyticNotation = value; + await this.plugin.saveSettings(); + }) + ); + + containerEl.createEl('h3', { text: 'Marqueur d\'export' }); + + new Setting(containerEl) + .setName('Ajouter un marqueur autour des pseudonymes dans l\'export') + .setDesc('Permet d\'identifier visuellement les termes pseudonymisés dans le fichier exporté') + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.useMarkerInExport).onChange(async (value) => { + this.plugin.settings.useMarkerInExport = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Marqueur ouvrant') + .setDesc('Exemple : ⟦ [ { «') + .addText((text) => + text.setValue(this.plugin.settings.markerOpen).onChange(async (value) => { + this.plugin.settings.markerOpen = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Marqueur fermant') + .setDesc('Exemple : ⟧ ] } »') + .addText((text) => + text.setValue(this.plugin.settings.markerClose).onChange(async (value) => { + this.plugin.settings.markerClose = value; + await this.plugin.saveSettings(); + }) + ); + + containerEl.createEl('h3', { text: 'Sécurité' }); + + new Setting(containerEl) + .setName('Avertir si le dossier est synchronisé') + .setDesc('Alerter si les tables de correspondance sont dans un dossier Git, iCloud ou Synology Drive') + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.warnIfSyncedFolder).onChange(async (value) => { + this.plugin.settings.warnIfSyncedFolder = value; + await this.plugin.saveSettings(); + }) + ); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0b22a6d --- /dev/null +++ b/src/types.ts @@ -0,0 +1,122 @@ +// Types partagés — SPECS §11.3 + +export type ScopeType = 'file' | 'folder' | 'vault'; + +export type MappingStatus = + | 'suggested' + | 'validated' + | 'ignored' + | 'partial' + | 'conflict' + | 'disabled' + | 'needs_review'; + +export type EntityCategory = + | 'first_name' + | 'last_name' + | 'full_name' + | 'place' + | 'institution' + | 'date' + | 'age' + | 'profession' + | 'custom'; + +export interface Scope { + type: ScopeType; + path?: string; +} + +export interface OccurrenceRef { + file: string; + line: number; + start: number; + end: number; + status: MappingStatus; +} + +export interface MappingRule { + id: string; + source: string; + replacement: string; + category: EntityCategory; + scope: Scope; + status: MappingStatus; + // Entier libre, comme un z-index CSS. Défaut 0. Plus élevé = appliqué en premier. + priority: number; + sourceDictionary?: string; + createdBy: 'user' | 'dictionary' | 'scanner'; + createdAt: string; // ISO 8601 + updatedAt?: string; + notes?: string; + occurrences?: OccurrenceRef[]; +} + +export interface Occurrence { + id: string; + file: string; + line: number; + start: number; + end: number; + text: string; + contextBefore: string; + contextAfter: string; + category?: EntityCategory; + mappingId?: string; + status: MappingStatus; +} + +export interface ReplacementSpan { + start: number; + end: number; + source: string; + replacement: string; + mappingId: string; + priority: number; +} + +// Format d'un fichier de table de correspondance (SPECS §5.2) +export interface MappingFile { + schemaVersion: string; + createdAt: string; + updatedAt: string; + project?: string; + scope: Scope; + settings?: MappingFileSettings; + mappings: MappingRule[]; +} + +export interface MappingFileSettings { + caseSensitive: boolean; + accentSensitive: boolean; + wholeWordOnly: boolean; + preserveCase: boolean; + preserveGender: boolean; + preserveAnalyticNotation: boolean; +} + +// Format d'une entrée de dictionnaire (SPECS §6.3) +export interface DictionaryEntry { + value: string; + type: EntityCategory; + gender?: 'masculine' | 'feminine' | 'mixed'; + decade?: number; // décennie de pic de popularité (ex. 1980) + socialClass?: 'populaire' | 'intermédiaire' | 'supérieur'; + origin?: string; // 'fr' | 'maghreb' | 'afrique-sub' | 'anglo' | … + sizeClass?: string; // pour les villes : 'village' | 'petite-ville' | … + frequencyRank?: number; + replacementCandidates?: string[]; + ambiguous?: string[]; // types alternatifs possibles (ex. ['place'] pour 'Florence') +} + +// Format d'un fichier dictionnaire (SPECS §6.3) +export interface DictionaryFile { + schemaVersion: string; + dictionaryId: string; + label: string; + type: EntityCategory; + language: string; + source: string; + license?: string; + entries: DictionaryEntry[]; +} diff --git a/src/ui/EditRuleModal.ts b/src/ui/EditRuleModal.ts new file mode 100644 index 0000000..ab25af7 --- /dev/null +++ b/src/ui/EditRuleModal.ts @@ -0,0 +1,120 @@ +import { App, Modal, Notice, Setting } from 'obsidian'; +import type PseudObsPlugin from '../main'; +import type { EntityCategory, MappingRule, ScopeType } from '../types'; +import type { RuleLocation } from '../mappings/ScopeResolver'; + +export class EditRuleModal extends Modal { + private plugin: PseudObsPlugin; + private location: RuleLocation; + + // Valeurs éditables — initialisées depuis la règle existante + private replacement: string; + private category: EntityCategory; + private scopeType: ScopeType; + private priority: number; + + constructor(app: App, plugin: PseudObsPlugin, location: RuleLocation) { + super(app); + this.plugin = plugin; + this.location = location; + const { rule } = location; + this.replacement = rule.replacement; + this.category = rule.category; + this.scopeType = rule.scope.type; + this.priority = rule.priority; + } + + onOpen(): void { + const { contentEl } = this; + const { rule } = this.location; + contentEl.createEl('h2', { text: 'Modifier la règle' }); + + new Setting(contentEl) + .setName('Source') + .setDesc('Non modifiable — créez une nouvelle règle pour changer la source') + .addText((t) => { + t.setValue(rule.source).setDisabled(true); + t.inputEl.style.opacity = '0.6'; + }); + + new Setting(contentEl) + .setName('Remplacement') + .addText((t) => + t.setValue(this.replacement).onChange((v) => (this.replacement = v)) + ); + + new Setting(contentEl) + .setName('Catégorie') + .addDropdown((d) => { + const options: Record = { + first_name: 'Prénom', last_name: 'Nom de famille', full_name: 'Nom complet', + place: 'Lieu', institution: 'Institution', date: 'Date', + age: 'Âge', profession: 'Profession', custom: 'Autre', + }; + for (const [value, label] of Object.entries(options)) d.addOption(value, label); + d.setValue(this.category); + d.onChange((v) => (this.category = v as EntityCategory)); + }); + + new Setting(contentEl) + .setName('Portée') + .addDropdown((d) => { + d.addOption('file', 'Ce fichier uniquement'); + d.addOption('folder', 'Ce dossier'); + d.addOption('vault', 'Tout le vault'); + d.setValue(this.scopeType); + d.onChange((v) => (this.scopeType = v as ScopeType)); + }); + + new Setting(contentEl) + .setName('Priorité') + .setDesc('Entier libre, comme un z-index CSS — défaut 0') + .addText((t) => + t.setValue(String(this.priority)).onChange((v) => (this.priority = parseInt(v, 10) || 0)) + ); + + new Setting(contentEl) + .addButton((btn) => + btn.setButtonText('Enregistrer').setCta().onClick(() => this.save()) + ) + .addButton((btn) => + btn + .setButtonText('Supprimer la règle') + .setWarning() + .onClick(() => this.delete()) + ); + } + + private async save(): Promise { + if (!this.replacement.trim()) { + new Notice('Le remplacement est obligatoire.'); + return; + } + + const { store, filePath, rule } = this.location; + store.update(rule.id, { + replacement: this.replacement.trim(), + category: this.category, + scope: { ...rule.scope, type: this.scopeType }, + priority: this.priority, + }); + + await this.plugin.scopeResolver.saveStore(store, filePath); + new Notice(`✓ Règle mise à jour : "${rule.source}" → "${this.replacement.trim()}"`); + this.plugin.refreshHighlightData(); + this.close(); + } + + private async delete(): Promise { + const { store, filePath, rule } = this.location; + store.remove(rule.id); + await this.plugin.scopeResolver.saveStore(store, filePath); + new Notice(`✓ Règle supprimée : "${rule.source}"`); + this.plugin.refreshHighlightData(); + this.close(); + } + + onClose(): void { + this.contentEl.empty(); + } +} diff --git a/src/ui/OccurrencesModal.ts b/src/ui/OccurrencesModal.ts new file mode 100644 index 0000000..bdae21b --- /dev/null +++ b/src/ui/OccurrencesModal.ts @@ -0,0 +1,258 @@ +import { App, Modal, Notice, Setting, TFile } from 'obsidian'; +import type PseudObsPlugin from '../main'; +import type { MappingRule, Occurrence, MappingStatus } from '../types'; +import { resolveSpans, applySpans } from '../pseudonymizer/SpanProtector'; +import type { ReplacementSpan } from '../types'; + +type Decision = 'validated' | 'ignored' | 'false_positive'; + +interface CardRef { + card: HTMLElement; + buttons: Map; + arrow: HTMLElement; + resLine: HTMLElement; + statusLabel: HTMLElement; +} + +export class OccurrencesModal extends Modal { + private plugin: PseudObsPlugin; + private file: TFile; + private content: string; + private occurrences: Occurrence[]; + private rules: MappingRule[]; + private decisions: Map = new Map(); + // Références stables — on ne recrée jamais les cartes + private cardRefs: Map = new Map(); + + constructor( + app: App, + plugin: PseudObsPlugin, + file: TFile, + content: string, + occurrences: Occurrence[], + rules: MappingRule[] + ) { + super(app); + this.plugin = plugin; + this.file = file; + this.content = content; + this.occurrences = occurrences; + this.rules = rules; + for (const occ of occurrences) this.decisions.set(occ.id, 'validated'); + } + + onOpen(): void { + const { contentEl } = this; + contentEl.createEl('h2', { text: `Scanner — ${this.file.name}` }); + + const n = this.occurrences.length; + contentEl.createEl('p', { + text: `${n} occurrence${n > 1 ? 's' : ''} trouvée${n > 1 ? 's' : ''} pour ${this.countRules()} règle${this.countRules() > 1 ? 's' : ''}.`, + }); + + // Légende + const legend = contentEl.createDiv(); + legend.style.cssText = 'display:flex;gap:16px;font-size:.8em;opacity:.65;margin-bottom:8px;flex-wrap:wrap;'; + for (const [icon, label, color] of [ + ['✓', 'Valider le remplacement', 'rgba(50,205,90,.7)'], + ['✗', 'Conserver l\'original', 'rgba(150,150,150,.7)'], + ['⚠', 'Faux positif — exclure', 'rgba(255,80,80,.6)'], + ] as [string, string, string][]) { + const item = legend.createSpan(); + item.style.cssText = `display:inline-flex;align-items:center;gap:4px;`; + const badge = item.createSpan({ text: icon }); + badge.style.cssText = `background:${color};border-radius:3px;padding:0 5px;font-weight:700;`; + item.createSpan({ text: ` ${label}` }); + } + + // Boutons globaux + new Setting(contentEl) + .addButton((b) => + b.setButtonText('✓ Tout valider').onClick(() => { + for (const occ of this.occurrences) this.decisions.set(occ.id, 'validated'); + this.updateAllCards(); + }) + ) + .addButton((b) => + b.setButtonText('✗ Tout ignorer').onClick(() => { + for (const occ of this.occurrences) this.decisions.set(occ.id, 'ignored'); + this.updateAllCards(); + }) + ); + + // Rendre les cartes une seule fois + this.renderAllCards(contentEl); + + contentEl.createEl('hr'); + + new Setting(contentEl).addButton((b) => + b.setButtonText('Appliquer').setCta().onClick(() => this.apply()) + ); + } + + private countRules(): number { + return new Set(this.occurrences.map((o) => o.mappingId)).size; + } + + // Construit toutes les cartes une seule fois — ne sera plus jamais appelé après + private renderAllCards(container: HTMLElement): void { + const byRule = new Map(); + for (const occ of this.occurrences) { + const key = occ.mappingId ?? ''; + if (!byRule.has(key)) byRule.set(key, []); + byRule.get(key)!.push(occ); + } + + for (const [mappingId, occs] of byRule) { + const rule = this.rules.find((r) => r.id === mappingId); + if (!rule) continue; + + const group = container.createDiv(); + group.createEl('div', { + text: `${rule.source} → ${rule.replacement}`, + }).style.cssText = 'font-weight:600;font-size:.9em;padding:4px 8px;background:var(--background-secondary);border-radius:4px;margin:12px 0 6px;'; + + for (const occ of occs) { + this.buildCard(group, occ, rule); + } + } + } + + private buildCard(container: HTMLElement, occ: Occurrence, rule: MappingRule): void { + const decision = this.decisions.get(occ.id) ?? 'validated'; + + const card = container.createDiv(); + this.applyCardStyle(card, decision); + + // Ligne source : terme surligné en jaune + const srcLine = card.createDiv(); + srcLine.style.cssText = 'font-family:var(--font-monospace);font-size:.85em;line-height:1.7;white-space:pre-wrap;word-break:break-word;'; + this.ctxSpan(srcLine, occ.contextBefore); + const termSpan = srcLine.createSpan({ text: occ.text }); + termSpan.style.cssText = 'background:rgba(255,210,0,.6);border-radius:3px;padding:1px 5px;font-weight:700;'; + this.ctxSpan(srcLine, occ.contextAfter); + + // Flèche + ligne résultat (masquées si ignoré ou faux positif) + const arrow = card.createDiv(); + arrow.style.cssText = 'font-size:.75em;opacity:.35;line-height:1.2;margin:1px 0;user-select:none;'; + arrow.setText('↓'); + + const resLine = card.createDiv(); + resLine.style.cssText = 'font-family:var(--font-monospace);font-size:.85em;line-height:1.7;white-space:pre-wrap;word-break:break-word;opacity:.8;'; + this.ctxSpan(resLine, occ.contextBefore); + const replSpan = resLine.createSpan({ text: rule.replacement }); + replSpan.style.cssText = 'background:rgba(50,205,90,.55);border-radius:3px;padding:1px 5px;font-weight:700;'; + this.ctxSpan(resLine, occ.contextAfter); + + // Label contextuel affiché quand la ligne résultat est masquée + const statusLabel = card.createDiv(); + statusLabel.style.cssText = 'font-size:.8em;font-style:italic;opacity:.55;margin:2px 0 4px;display:none;'; + + // Méta + const meta = card.createEl('small'); + meta.style.cssText = 'display:block;font-size:.75em;opacity:.45;margin-top:4px;'; + meta.setText(`ligne ${occ.line}`); + + // Boutons — créés une seule fois, mis à jour via updateCard() + const actions = card.createDiv(); + actions.style.cssText = 'display:flex;gap:6px;margin-top:6px;'; + + const btnRefs = new Map(); + for (const [label, value, title] of [ + ['✓', 'validated', 'Valider'], + ['✗', 'ignored', 'Ignorer'], + ['⚠', 'false_positive', 'Faux positif'], + ] as [string, Decision, string][]) { + const btn = actions.createEl('button', { text: label }); + btn.title = title; + this.applyBtnStyle(btn, value === decision); + btn.addEventListener('click', () => { + this.decisions.set(occ.id, value); + this.updateCard(occ.id); + }); + btnRefs.set(value, btn); + } + + this.cardRefs.set(occ.id, { card, buttons: btnRefs, arrow, resLine, statusLabel }); + // Appliquer l'état initial + this.updateCard(occ.id); + } + + // Met à jour UNE carte sans toucher au DOM — styles + résultat en direct + private updateCard(occId: string): void { + const ref = this.cardRefs.get(occId); + if (!ref) return; + const decision = this.decisions.get(occId) ?? 'validated'; + + this.applyCardStyle(ref.card, decision); + + for (const [value, btn] of ref.buttons) { + this.applyBtnStyle(btn, value === decision); + } + + // Afficher la ligne résultat seulement si l'occurrence est validée + const show = decision === 'validated'; + ref.arrow.style.display = show ? '' : 'none'; + ref.resLine.style.display = show ? '' : 'none'; + + // Label contextuel pour les cas ignoré / faux positif + const labels: Record = { + validated: '', + ignored: 'Conservé tel quel dans ce fichier', + false_positive: 'Faux positif — exclu du remplacement', + }; + ref.statusLabel.style.display = show ? 'none' : ''; + ref.statusLabel.setText(labels[decision]); + } + + // Met à jour TOUTES les cartes + private updateAllCards(): void { + for (const occId of this.cardRefs.keys()) { + this.updateCard(occId); + } + } + + private applyCardStyle(card: HTMLElement, decision: Decision): void { + const borders: Record = { + validated: 'rgba(60,200,100,.7)', + ignored: 'rgba(150,150,150,.4)', + false_positive:'rgba(255,80,80,.6)', + }; + const opacity = decision === 'validated' ? '1' : '0.55'; + card.style.cssText = `border:1px solid var(--background-modifier-border);border-left:3px solid ${borders[decision]};border-radius:6px;padding:8px 10px;margin:4px 0;opacity:${opacity};`; + } + + private applyBtnStyle(btn: HTMLElement, active: boolean): void { + btn.style.cssText = `padding:2px 10px;border-radius:4px;cursor:pointer;font-size:.85em;border:1px solid var(--background-modifier-border);background:${active ? 'var(--interactive-accent)' : 'var(--background-primary)'};color:${active ? 'var(--text-on-accent)' : 'var(--text-normal)'};`; + } + + private ctxSpan(parent: HTMLElement, text: string): void { + const s = parent.createSpan({ text }); + s.style.opacity = '0.5'; + } + + private async apply(): Promise { + const validated = this.occurrences.filter((o) => this.decisions.get(o.id) === 'validated'); + const ignored = this.occurrences.filter((o) => this.decisions.get(o.id) === 'ignored'); + + const spans: ReplacementSpan[] = validated.map((occ) => { + const rule = this.rules.find((r) => r.id === occ.mappingId)!; + return { start: occ.start, end: occ.end, source: occ.text, replacement: rule.replacement, mappingId: occ.mappingId ?? '', priority: rule.priority }; + }); + + const updated = applySpans(this.content, resolveSpans(spans)); + await this.app.vault.modify(this.file, updated); + + await this.plugin.updateMappingStatuses(this.file.path, this.rules, this.occurrences, this.decisions); + + const nv = validated.length, ni = ignored.length; + new Notice(`✓ ${nv} remplacement${nv > 1 ? 's' : ''} appliqué${nv > 1 ? 's' : ''}` + (ni > 0 ? `, ${ni} ignoré${ni > 1 ? 's' : ''}` : '')); + + this.plugin.refreshHighlightData(); + this.close(); + } + + onClose(): void { + this.contentEl.empty(); + } +} diff --git a/src/ui/PseudonymHighlighter.ts b/src/ui/PseudonymHighlighter.ts new file mode 100644 index 0000000..dd7ad73 --- /dev/null +++ b/src/ui/PseudonymHighlighter.ts @@ -0,0 +1,79 @@ +import { Extension, RangeSetBuilder, StateEffect } from '@codemirror/state'; +import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'; + +// Signal externe : les données de surlignage ont changé. +// À dispatcher sur l'EditorView après chaque refreshHighlightData(). +export const highlightDataChanged = StateEffect.define(); + +export interface HighlightData { + sources: string[]; // termes originaux encore présents → orange (à pseudonymiser) + replacements: string[]; // pseudonymes déjà appliqués → vert +} + +// Extension CodeMirror 6 qui surligne dans l'éditeur : +// - en orange : les termes sources (à pseudonymiser) +// - en vert : les termes de remplacement (déjà pseudonymisés) +// getData est appelé de façon synchrone à chaque mise à jour du doc. +export function createPseudonymHighlighter(getData: () => HighlightData): Extension { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet = Decoration.none; + + constructor(view: EditorView) { + this.decorations = this.build(view); + } + + update(update: ViewUpdate): void { + const dataChanged = update.transactions.some((t) => + t.effects.some((e) => e.is(highlightDataChanged)) + ); + if (update.docChanged || update.viewportChanged || dataChanged) { + this.decorations = this.build(update.view); + } + } + + private build(view: EditorView): DecorationSet { + const { sources, replacements } = getData(); + if (sources.length === 0 && replacements.length === 0) return Decoration.none; + + const text = view.state.doc.toString(); + const lower = text.toLowerCase(); + + type Span = { from: number; to: number; cls: string }; + const spans: Span[] = []; + + const collect = (terms: string[], cls: string) => { + for (const term of terms) { + if (!term) continue; + const needle = term.toLowerCase(); + let pos = 0; + while (pos < lower.length) { + const idx = lower.indexOf(needle, pos); + if (idx === -1) break; + spans.push({ from: idx, to: idx + term.length, cls }); + pos = idx + term.length; + } + } + }; + + collect(sources, 'pseudobs-source'); + collect(replacements, 'pseudobs-replaced'); + + // Trier par position (RangeSetBuilder l'exige) et éliminer les chevauchements + spans.sort((a, b) => a.from - b.from || a.to - b.to); + + const builder = new RangeSetBuilder(); + let lastTo = -1; + for (const { from, to, cls } of spans) { + if (from >= lastTo) { + builder.add(from, to, Decoration.mark({ class: cls })); + lastTo = to; + } + } + + return builder.finish(); + } + }, + { decorations: (v) => v.decorations } + ); +} diff --git a/src/ui/QuickPseudonymizeModal.ts b/src/ui/QuickPseudonymizeModal.ts new file mode 100644 index 0000000..572415f --- /dev/null +++ b/src/ui/QuickPseudonymizeModal.ts @@ -0,0 +1,200 @@ +import { App, Editor, EditorPosition, Modal, Notice, Setting, TFile } from 'obsidian'; +import type PseudObsPlugin from '../main'; +import { MappingStore } from '../mappings/MappingStore'; +import { PseudonymizationEngine } from '../pseudonymizer/PseudonymizationEngine'; +import type { EntityCategory, MappingFile } from '../types'; + +type ApplyScope = 'occurrence' | 'file'; + +export class QuickPseudonymizeModal extends Modal { + private plugin: PseudObsPlugin; + private editor: Editor; + private source: string; + private from: EditorPosition; + private to: EditorPosition; + + private replacement = ''; + private category: EntityCategory = 'custom'; + private applyScope: ApplyScope = 'file'; + private suggestions: string[]; + + constructor( + app: App, + plugin: PseudObsPlugin, + editor: Editor, + prefillReplacement = '', + suggestions: string[] = [] + ) { + super(app); + this.plugin = plugin; + this.editor = editor; + this.source = editor.getSelection(); + this.replacement = prefillReplacement; + this.suggestions = suggestions; + // Coulmont ne traite que des prénoms + if (suggestions.length > 0) this.category = 'first_name'; + this.from = editor.getCursor('from'); + this.to = editor.getCursor('to'); + } + + onOpen(): void { + const { contentEl } = this; + contentEl.createEl('h2', { text: 'Pseudonymiser' }); + + // Source en lecture seule + new Setting(contentEl) + .setName('Expression sélectionnée') + .setDesc('Terme à remplacer — non modifiable') + .addText((t) => { + t.setValue(this.source).setDisabled(true); + t.inputEl.style.opacity = '0.6'; + }); + + // Suggestions Coulmont : boutons cliquables qui remplissent le champ + let replacementInput: HTMLInputElement; + if (this.suggestions.length > 0) { + const suggBox = contentEl.createDiv(); + suggBox.style.cssText = 'margin-bottom:6px;'; + suggBox.createEl('small', { text: 'Suggestions Coulmont (le jeu de données ne différencie pas M/F) :' }) + .style.cssText = 'display:block;opacity:.6;margin-bottom:4px;font-size:.8em;'; + const tags = suggBox.createDiv(); + tags.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;'; + for (const name of this.suggestions) { + const btn = tags.createEl('button', { text: name }); + btn.style.cssText = 'padding:2px 10px;border-radius:12px;border:1px solid var(--background-modifier-border);cursor:pointer;font-size:.85em;background:var(--background-secondary);'; + btn.addEventListener('click', () => { + this.replacement = name; + if (replacementInput) { + replacementInput.value = name; + replacementInput.dispatchEvent(new Event('input')); + } + // Mettre en évidence le bouton sélectionné + tags.querySelectorAll('button').forEach((b) => { + (b as HTMLElement).style.background = 'var(--background-secondary)'; + (b as HTMLElement).style.fontWeight = 'normal'; + }); + btn.style.background = 'var(--interactive-accent)'; + btn.style.color = 'var(--text-on-accent)'; + btn.style.fontWeight = '600'; + }); + } + } + + // Champ de remplacement — pré-rempli si un prénom a déjà été sélectionné + new Setting(contentEl) + .setName('Remplacer par') + .addText((t) => { + t.setPlaceholder('pseudonyme ou catégorie analytique'); + t.setValue(this.replacement); + t.onChange((v) => (this.replacement = v)); + replacementInput = t.inputEl; + }); + + new Setting(contentEl) + .setName('Catégorie') + .addDropdown((d) => { + const options: Record = { + first_name: 'Prénom', + last_name: 'Nom de famille', + full_name: 'Nom complet', + place: 'Lieu', + institution: 'Institution', + date: 'Date', + age: 'Âge', + profession: 'Profession', + custom: 'Autre', + }; + for (const [value, label] of Object.entries(options)) { + d.addOption(value, label); + } + d.setValue('custom'); + d.onChange((v) => (this.category = v as EntityCategory)); + }); + + new Setting(contentEl) + .setName('Portée du remplacement') + .addDropdown((d) => { + d.addOption('file', 'Toutes les occurrences dans ce fichier'); + d.addOption('occurrence', 'Cette occurrence uniquement'); + d.setValue('file'); + d.onChange((v) => (this.applyScope = v as ApplyScope)); + }); + + new Setting(contentEl).addButton((btn) => + btn + .setButtonText('Pseudonymiser') + .setCta() + .onClick(() => this.apply()) + ); + + // Focus sur le champ de remplacement à l'ouverture + setTimeout(() => replacementInput?.focus(), 50); + } + + private async apply(): Promise { + const replacement = this.replacement.trim(); + if (!replacement) { + new Notice('Le remplacement est obligatoire.'); + return; + } + + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + new Notice('Aucun fichier actif.'); + return; + } + + // 1. Sauvegarder la règle dans le mapping JSON + await this.saveRule(activeFile, replacement); + + // 2. Appliquer dans le fichier + if (this.applyScope === 'occurrence') { + // Remplacer uniquement la sélection courante dans l'éditeur + this.editor.replaceRange(replacement, this.from, this.to); + new Notice(`✓ "${this.source}" → "${replacement}" (cette occurrence)`); + } else { + // Remplacer toutes les occurrences dans le fichier courant + const count = await this.plugin.applyRuleToFile(activeFile, this.source, replacement); + new Notice(`✓ "${this.source}" → "${replacement}" (${count} occurrence${count > 1 ? 's' : ''})`); + } + + // Rafraîchir le surlignage immédiatement + this.plugin.refreshHighlightData(); + this.close(); + } + + private async saveRule(activeFile: TFile, replacement: string): Promise { + const mappingPath = `${this.plugin.settings.mappingFolder}/${activeFile.basename}.mapping.json`; + let store: MappingStore; + + const mappingTFile = this.app.vault.getAbstractFileByPath(mappingPath); + if (mappingTFile instanceof TFile) { + const data: MappingFile = JSON.parse(await this.app.vault.read(mappingTFile)); + store = MappingStore.fromJSON(data); + } else { + await this.plugin.ensureFolder(this.plugin.settings.mappingFolder); + store = new MappingStore({ type: 'file', path: activeFile.path }); + } + + store.add({ + source: this.source, + replacement, + category: this.category, + scope: { type: 'file', path: activeFile.path }, + status: 'validated', + priority: 0, + createdBy: 'user', + }); + + const json = JSON.stringify(store.toJSON(), null, 2); + if (mappingTFile instanceof TFile) { + await this.app.vault.modify(mappingTFile, json); + } else { + await this.app.vault.create(mappingPath, json); + } + } + + onClose(): void { + this.contentEl.empty(); + } +} diff --git a/src/ui/RuleModal.ts b/src/ui/RuleModal.ts new file mode 100644 index 0000000..453577a --- /dev/null +++ b/src/ui/RuleModal.ts @@ -0,0 +1,187 @@ +import { App, Modal, Setting, TFile, Notice } from 'obsidian'; +import type PseudObsPlugin from '../main'; +import { MappingStore } from '../mappings/MappingStore'; +import type { EntityCategory, MappingFile, ScopeType } from '../types'; + +export class RuleModal extends Modal { + private plugin: PseudObsPlugin; + private source: string; + private replacement: string; + private category: EntityCategory; + private scopeType: ScopeType = 'file'; + private priority = 0; + private suggestions: string[]; + + constructor( + app: App, + plugin: PseudObsPlugin, + prefillSource = '', + prefillReplacement = '', + suggestions: string[] = [] + ) { + super(app); + this.plugin = plugin; + this.source = prefillSource; + this.replacement = prefillReplacement; + this.suggestions = suggestions; + // Coulmont ne fournit que des prénoms + this.category = suggestions.length > 0 ? 'first_name' : 'custom'; + } + + onOpen(): void { + const { contentEl } = this; + contentEl.createEl('h2', { text: 'Créer une règle de remplacement' }); + + new Setting(contentEl) + .setName('Source') + .setDesc('Terme original à remplacer') + .addText((t) => + t.setValue(this.source).onChange((v) => { + this.source = v; + }) + ); + + // Suggestions Coulmont : boutons cliquables (présents uniquement si fournis) + let replacementInput: HTMLInputElement | undefined; + if (this.suggestions.length > 0) { + const box = contentEl.createDiv(); + box.style.cssText = 'margin-bottom:6px;'; + box.createEl('small', { text: 'Suggestions Coulmont (M/F non différencié — choisissez) :' }) + .style.cssText = 'display:block;opacity:.6;margin-bottom:4px;font-size:.8em;'; + const tags = box.createDiv(); + tags.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px;'; + for (const name of this.suggestions) { + const btn = tags.createEl('button', { text: name }); + btn.style.cssText = 'padding:2px 10px;border-radius:12px;border:1px solid var(--background-modifier-border);cursor:pointer;font-size:.85em;background:var(--background-secondary);'; + btn.addEventListener('click', () => { + this.replacement = name; + if (replacementInput) replacementInput.value = name; + tags.querySelectorAll('button').forEach((b) => { + (b as HTMLElement).style.cssText = 'padding:2px 10px;border-radius:12px;border:1px solid var(--background-modifier-border);cursor:pointer;font-size:.85em;background:var(--background-secondary);'; + }); + btn.style.cssText = 'padding:2px 10px;border-radius:12px;border:1px solid var(--interactive-accent);cursor:pointer;font-size:.85em;background:var(--interactive-accent);color:var(--text-on-accent);font-weight:600;'; + }); + } + } + + new Setting(contentEl) + .setName('Remplacement') + .setDesc('Pseudonyme ou catégorie analytique') + .addText((t) => { + t.setValue(this.replacement).onChange((v) => { this.replacement = v; }); + replacementInput = t.inputEl; + }); + + // Catégorie — masquée et fixée à "Prénom" si suggestions Coulmont + new Setting(contentEl) + .setName('Catégorie') + .addDropdown((d) => { + const options: Record = { + first_name: 'Prénom', + last_name: 'Nom de famille', + full_name: 'Nom complet', + place: 'Lieu', + institution: 'Institution', + date: 'Date', + age: 'Âge', + profession: 'Profession', + custom: 'Autre', + }; + for (const [value, label] of Object.entries(options)) { + d.addOption(value, label); + } + d.setValue(this.category); + d.onChange((v) => { this.category = v as EntityCategory; }); + // Masquer le dropdown si la catégorie est imposée par Coulmont + if (this.suggestions.length > 0) { + d.selectEl.closest('.setting-item')?.setAttribute('style', 'display:none'); + } + }); + + new Setting(contentEl) + .setName('Portée') + .addDropdown((d) => { + d.addOption('file', 'Ce fichier uniquement'); + d.addOption('folder', 'Ce dossier'); + d.addOption('vault', 'Tout le vault'); + d.setValue('file'); + d.onChange((v) => { + this.scopeType = v as ScopeType; + }); + }); + + new Setting(contentEl) + .setName('Priorité') + .setDesc('Entier libre, comme un z-index CSS — défaut 0, plus grand = appliqué en premier') + .addText((t) => + t.setValue('0').onChange((v) => { + this.priority = parseInt(v, 10) || 0; + }) + ); + + new Setting(contentEl).addButton((btn) => + btn + .setButtonText('Créer la règle') + .setCta() + .onClick(() => this.createRule()) + ); + } + + private async createRule(): Promise { + if (!this.source.trim() || !this.replacement.trim()) { + new Notice('La source et le remplacement sont obligatoires.'); + return; + } + + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + new Notice('Aucun fichier actif.'); + return; + } + + const mappingPath = `${this.plugin.settings.mappingFolder}/${activeFile.basename}.mapping.json`; + let store: MappingStore; + + const mappingFile = this.app.vault.getAbstractFileByPath(mappingPath); + if (mappingFile instanceof TFile) { + const data: MappingFile = JSON.parse(await this.app.vault.read(mappingFile)); + store = MappingStore.fromJSON(data); + } else { + await this.plugin.ensureFolder(this.plugin.settings.mappingFolder); + store = new MappingStore({ type: 'file', path: activeFile.path }); + } + + const scopePath = + this.scopeType === 'file' + ? activeFile.path + : this.scopeType === 'folder' + ? (activeFile.parent?.path ?? '') + : undefined; + + store.add({ + source: this.source.trim(), + replacement: this.replacement.trim(), + category: this.category, + scope: { type: this.scopeType, path: scopePath }, + status: 'validated', + priority: this.priority, + createdBy: 'user', + }); + + const json = JSON.stringify(store.toJSON(), null, 2); + if (mappingFile instanceof TFile) { + await this.app.vault.modify(mappingFile, json); + } else { + await this.app.vault.create(mappingPath, json); + } + + new Notice(`✓ Règle créée : "${this.source.trim()}" → "${this.replacement.trim()}"`); + // Rafraîchir le surlignage immédiatement sans attendre un changement de fichier + this.plugin.refreshHighlightData(); + this.close(); + } + + onClose(): void { + this.contentEl.empty(); + } +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..16036fb --- /dev/null +++ b/styles.css @@ -0,0 +1,94 @@ +/* PseudObsidian-ization — styles */ + +/* Termes sources encore présents dans le texte (à pseudonymiser) */ +.pseudobs-source { + background-color: rgba(255, 160, 0, 0.22); + border-radius: 2px; + outline: 1px solid rgba(255, 160, 0, 0.4); +} + +/* Termes de remplacement déjà appliqués (pseudonymes) */ +.pseudobs-replaced { + background-color: rgba(60, 200, 100, 0.2); + border-radius: 2px; + outline: 1px solid rgba(60, 200, 100, 0.35); +} + +/* Modal de validation sélective */ +.pseudobs-scan-summary { opacity: 0.7; margin-bottom: 0.5em; } + +.pseudobs-occ-group { margin: 1em 0; } + +.pseudobs-occ-rule-header { + font-weight: 600; + font-size: 0.9em; + padding: 4px 8px; + background: var(--background-secondary); + border-radius: 4px; + margin-bottom: 6px; +} + +.pseudobs-occ-card { + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + padding: 8px 10px; + margin: 4px 0; + transition: background 0.15s; +} +.pseudobs-occ-validated { border-left: 3px solid rgba(60, 200, 100, 0.7); } +.pseudobs-occ-ignored { border-left: 3px solid rgba(150, 150, 150, 0.5); opacity: 0.6; } +.pseudobs-occ-false_positive { border-left: 3px solid rgba(255, 80, 80, 0.6); opacity: 0.5; } + +.pseudobs-occ-line { + font-family: var(--font-monospace); + font-size: 0.85em; + line-height: 1.7; + white-space: pre-wrap; + word-break: break-word; +} +.pseudobs-occ-result-line { opacity: 0.8; } + +/* Terme source → jaune */ +.pseudobs-occ-term { + background: rgba(255, 210, 0, 0.55); + border-radius: 3px; + padding: 1px 5px; + font-weight: 700; + color: var(--text-normal); +} + +/* Remplacement proposé → vert */ +.pseudobs-occ-replacement { + background: rgba(50, 205, 90, 0.5); + border-radius: 3px; + padding: 1px 5px; + font-weight: 700; + color: var(--text-normal); +} + +.pseudobs-ctx-side { opacity: 0.55; } + +.pseudobs-occ-arrow { + font-size: 0.75em; + opacity: 0.35; + line-height: 1.2; + padding-left: 2px; + user-select: none; +} + +.pseudobs-occ-meta { display: block; font-size: 0.75em; opacity: 0.45; margin-top: 4px; } + +.pseudobs-occ-actions { display: flex; gap: 6px; margin-top: 6px; } +.pseudobs-occ-btn { + padding: 2px 10px; + border-radius: 4px; + border: 1px solid var(--background-modifier-border); + cursor: pointer; + font-size: 0.85em; + background: var(--background-primary); +} +.pseudobs-occ-btn-active { + background: var(--interactive-accent); + color: var(--text-on-accent); + border-color: var(--interactive-accent); +} diff --git a/tests/__mocks__/obsidian.ts b/tests/__mocks__/obsidian.ts new file mode 100644 index 0000000..f3543c0 --- /dev/null +++ b/tests/__mocks__/obsidian.ts @@ -0,0 +1,49 @@ +// Mock minimal de l'API Obsidian pour Jest (environnement Node, pas navigateur) + +export class Plugin { + app: App = new App(); + async loadData(): Promise { return {}; } + async saveData(_data: unknown): Promise {} + addSettingTab(_tab: unknown): void {} +} + +export class App {} + +export class PluginSettingTab { + app: App; + containerEl: MockEl; + + constructor(app: App, _plugin: unknown) { + this.app = app; + this.containerEl = new MockEl(); + } +} + +export class Setting { + constructor(_containerEl: unknown) {} + setName(_name: string): this { return this; } + setDesc(_desc: string): this { return this; } + addText(_cb: (_t: MockTextInput) => unknown): this { + _cb(new MockTextInput()); + return this; + } + addToggle(_cb: (_t: MockToggle) => unknown): this { + _cb(new MockToggle()); + return this; + } +} + +class MockEl { + empty(): void {} + createEl(_tag: string, _opts?: unknown): MockEl { return new MockEl(); } +} + +class MockTextInput { + setValue(_v: string): this { return this; } + onChange(_cb: (_v: string) => unknown): this { return this; } +} + +class MockToggle { + setValue(_v: boolean): this { return this; } + onChange(_cb: (_v: boolean) => unknown): this { return this; } +} diff --git a/tests/fixtures/entretien_01.srt b/tests/fixtures/entretien_01.srt new file mode 100644 index 0000000..7823c8b --- /dev/null +++ b/tests/fixtures/entretien_01.srt @@ -0,0 +1,23 @@ +1 +00:00:01,000 --> 00:00:04,500 +Bonjour Jean, tu peux te présenter ? + +2 +00:00:05,000 --> 00:00:09,200 +Je m'appelle Jean Dupont, j'habite Saint-Jean-de-Luz. + +3 +00:00:10,000 --> 00:00:13,500 +Et tu travailles au CHU de Montpellier ? + +4 +00:00:14,000 --> 00:00:17,800 +Oui, au CHU de Montpellier, depuis trois ans. + +5 +00:00:18,200 --> 00:00:22,000 +Tu connais Paul Martin, qui travaille à Saint-Paul ? + +6 +00:00:22,500 --> 00:00:26,300 +Oui, on s'est rencontrés à Montpellier, à la fac de médecine. diff --git a/tests/fixtures/entretien_02.cha b/tests/fixtures/entretien_02.cha new file mode 100644 index 0000000..227dd9e --- /dev/null +++ b/tests/fixtures/entretien_02.cha @@ -0,0 +1,19 @@ +@Begin +@Languages: fra +@Participants: INV Investigatrice adulte, PAR Participant adulte +@ID: fra|corpus|INV|adulte||Investigatrice|||adulte| +@ID: fra|corpus|PAR|adulte||Participant|||adulte| +@Date: 11-MAY-2026 +@Location: Lyon +@Transcriber: AA +*INV: bonjour Marie, tu peux te présenter ? +*PAR: je m'appelle Marie Dupont, j'ai trente-deux ans . +*INV: et tu habites à Lyon ? +*PAR: oui, dans le quartier de la Guillotière . +%com: PAR marque une hésitation avant de répondre +*INV: tu travailles dans quel secteur ? +*PAR: à l' hôpital (.) au service de pédiatrie . +*INV: et tu as grandi à Nancy ? +*PAR: oui (0.5) à Nancy (.) la ville (.) pas le prénom . +%com: INV et PAR rient +@End diff --git a/tests/fixtures/entretien_03.md b/tests/fixtures/entretien_03.md new file mode 100644 index 0000000..e11fdd7 --- /dev/null +++ b/tests/fixtures/entretien_03.md @@ -0,0 +1,21 @@ +# Entretien 03 — Transcription Jefferson + +**Corpus** : UJAA-2026 +**Enquêtrice** : INV +**Participant** : PAR (Luc Bernard, 41 ans, enseignant, né à Nancy) + +--- + +INV : bonjour [Luc, +PAR : [oui bonjour + +INV : tu peux me parler de ton parcours ? +PAR : bien sûr (0.5) je suis né à Nancy en mille neuf cent quatre-vingt-cinq: +INV : à Nancy ? +PAR : oui (.) la ville (.) pas le prénom ((rires)) +INV : et maintenant tu vis à Lyon ? +PAR : voilà °à Lyon° depuis deux ans= +INV : =dans quel quartier ? +PAR : .hh euh (1.2) la Croix-Rousse +INV : et tu travailles à: (.) l'université ? +PAR : oui à l'université Lyon 2 en fait diff --git a/tests/unit/ChatParser.test.ts b/tests/unit/ChatParser.test.ts new file mode 100644 index 0000000..bd019fe --- /dev/null +++ b/tests/unit/ChatParser.test.ts @@ -0,0 +1,141 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { ChatParser } from '../../src/parsers/ChatParser'; +import type { ChatLine } from '../../src/parsers/ChatParser'; + +const FIXTURE = path.join(__dirname, '../fixtures/entretien_02.cha'); + +describe('ChatParser', () => { + const parser = new ChatParser(); + let content: string; + let lines: ChatLine[]; + + beforeAll(() => { + content = fs.readFileSync(FIXTURE, 'utf-8'); + lines = parser.parse(content).lines; + }); + + // --- Structure (SPECS §3.3) --- + + it('parse le bon nombre de lignes', () => { + // 8 meta (@Begin…@Transcriber) + 8 turns + 2 dependent + 1 meta (@End) = 19 + expect(lines).toHaveLength(19); + }); + + it('identifie correctement les lignes @ comme meta', () => { + const metas = lines.filter((l) => l.type === 'meta'); + expect(metas).toHaveLength(9); // @Begin, 7 @*, @End + expect(metas[0].raw).toBe('@Begin'); + expect(metas[metas.length - 1].raw).toBe('@End'); + }); + + it('identifie correctement les tours de parole (*)', () => { + const turns = lines.filter((l) => l.type === 'turn'); + expect(turns).toHaveLength(8); + }); + + it('identifie correctement les lignes dépendantes (%)', () => { + const deps = lines.filter((l) => l.type === 'dependent'); + expect(deps).toHaveLength(2); + expect(deps[0].raw).toContain('%com:'); + expect(deps[1].raw).toContain('%com:'); + }); + + // --- Extraction locuteur / contenu --- + + it('extrait le locuteur et le contenu du premier tour INV', () => { + const firstTurn = lines.find((l) => l.type === 'turn'); + expect(firstTurn?.speaker).toBe('INV'); + expect(firstTurn?.content).toBe('bonjour Marie, tu peux te présenter ?'); + }); + + it('extrait le locuteur et le contenu du premier tour PAR (Marie Dupont)', () => { + const parTurns = lines.filter((l) => l.type === 'turn' && l.speaker === 'PAR'); + expect(parTurns[0].content).toContain('Marie Dupont'); + }); + + it('le prefix reconstitue la ligne originale avec le contenu', () => { + const turns = lines.filter((l) => l.type === 'turn'); + for (const turn of turns) { + expect(turn.prefix! + turn.content!).toBe(turn.raw); + } + }); + + // --- Round-trip exact (SPECS §17.5) --- + + it('round-trip exact : reconstruct(parse(content)) === content', () => { + const doc = parser.parse(content); + expect(parser.reconstruct(doc)).toBe(content); + }); + + it('les lignes @ sont inchangées après reconstruction', () => { + const doc = parser.parse(content); + const result = parser.reconstruct(doc); + expect(result).toContain('@Begin'); + expect(result).toContain('@End'); + expect(result).toContain('@Languages: fra'); + expect(result).toContain('@Participants:'); + }); + + it('les lignes % sont inchangées après reconstruction', () => { + const doc = parser.parse(content); + const result = parser.reconstruct(doc); + expect(result).toContain('%com: PAR marque une hésitation avant de répondre'); + expect(result).toContain('%com: INV et PAR rient'); + }); + + // --- Remplacement de contenu --- + + it('modifier le contenu ne touche pas le locuteur ni les lignes @', () => { + const doc = parser.parse(content); + const parTurn = doc.lines.find((l) => l.type === 'turn' && l.speaker === 'PAR'); + parTurn!.content = parTurn!.content!.replace('Marie Dupont', 'Sophie Arnaud'); + const result = parser.reconstruct(doc); + expect(result).toContain('*PAR: je m\'appelle Sophie Arnaud'); + expect(result).toContain('@Begin'); + expect(result).toContain('@Participants:'); + expect(result).not.toContain('Marie Dupont'); + }); + + it("le cas Nancy ville/prenom : PAR thematise l'ambiguite dans son tour", () => { + // INV mentionne Nancy en premier ; PAR explicite l'ambiguïté ville/prénom + const parNancyTurn = lines.find( + (l) => l.type === 'turn' && l.speaker === 'PAR' && l.content?.includes('Nancy') + ); + expect(parNancyTurn).toBeDefined(); + expect(parNancyTurn?.content).toContain('Nancy'); + expect(parNancyTurn?.content).toContain('la ville'); + expect(parNancyTurn?.content).toContain('le prénom'); + }); + + // --- Robustesse --- + + it('parse un fichier CHAT avec fins de ligne Windows (CRLF)', () => { + const crlf = content.replace(/\n/g, '\r\n'); + const doc = parser.parse(crlf); + expect(doc.lines).toHaveLength(19); + expect(doc.lines[0].type).toBe('meta'); + expect(doc.lines[0].raw).toBe('@Begin'); + }); + + it('parse un fichier CHAT sans saut de ligne final', () => { + const noTrailing = content.trimEnd(); + const doc = parser.parse(noTrailing); + expect(doc.trailingNewline).toBe(false); + expect(parser.reconstruct(doc)).toBe(noTrailing); + }); + + it('un locuteur avec tabulation comme séparateur est correctement parsé', () => { + const withTab = '*INV:\tbonjour tout le monde'; + const line = (parser as unknown as { parseLine: (r: string) => ChatLine }).parseLine?.(withTab) + ?? parser.parse(withTab + '\n@End\n').lines[0]; + // fallback : on teste via un mini-document + const miniDoc = parser.parse('@Begin\n' + withTab + '\n@End\n'); + const turn = miniDoc.lines.find((l) => l.type === 'turn'); + expect(turn?.speaker).toBe('INV'); + expect(turn?.content).toBe('bonjour tout le monde'); + expect(turn?.prefix).toBe('*INV:\t'); + // round-trip du mini-document + expect(parser.reconstruct(miniDoc)).toBe('@Begin\n' + withTab + '\n@End\n'); + }); +}); diff --git a/tests/unit/MappingStore.test.ts b/tests/unit/MappingStore.test.ts new file mode 100644 index 0000000..cb5b207 --- /dev/null +++ b/tests/unit/MappingStore.test.ts @@ -0,0 +1,147 @@ +import { MappingStore, DEFAULT_FILE_SETTINGS } from '../../src/mappings/MappingStore'; +import type { MappingFile } from '../../src/types'; + +const FILE_SCOPE = { type: 'file' as const, path: 'entretien_01.srt' }; + +function makeRule(source: string, replacement: string) { + return { + source, + replacement, + category: 'first_name' as const, + scope: FILE_SCOPE, + status: 'validated' as const, + priority: 0, + createdBy: 'user' as const, + }; +} + +describe('MappingStore', () => { + // --- CRUD --- + + it('add retourne une règle avec un id unique', () => { + const store = new MappingStore(FILE_SCOPE); + const rule = store.add(makeRule('Jean', 'Pierre')); + expect(rule.id).toMatch(/^map_/); + expect(rule.source).toBe('Jean'); + expect(rule.createdAt).toBeTruthy(); + }); + + it('deux règles ajoutées ont des ids distincts', () => { + const store = new MappingStore(FILE_SCOPE); + const r1 = store.add(makeRule('Jean', 'Pierre')); + const r2 = store.add(makeRule('Marie', 'Sophie')); + expect(r1.id).not.toBe(r2.id); + }); + + it('getAll retourne toutes les règles ajoutées', () => { + const store = new MappingStore(FILE_SCOPE); + store.add(makeRule('Jean', 'Pierre')); + store.add(makeRule('Marie', 'Sophie')); + expect(store.getAll()).toHaveLength(2); + }); + + it('get retourne la règle par son id', () => { + const store = new MappingStore(FILE_SCOPE); + const rule = store.add(makeRule('Jean', 'Pierre')); + expect(store.get(rule.id)?.source).toBe('Jean'); + }); + + it('get retourne undefined pour un id inexistant', () => { + const store = new MappingStore(FILE_SCOPE); + expect(store.get('inexistant')).toBeUndefined(); + }); + + it('update modifie le remplacement', () => { + const store = new MappingStore(FILE_SCOPE); + const rule = store.add(makeRule('Jean', 'Pierre')); + store.update(rule.id, { replacement: 'Paul' }); + expect(store.get(rule.id)?.replacement).toBe('Paul'); + }); + + it('update retourne false si la règle est introuvable', () => { + const store = new MappingStore(FILE_SCOPE); + expect(store.update('inexistant', { replacement: 'X' })).toBe(false); + }); + + it('remove supprime la règle', () => { + const store = new MappingStore(FILE_SCOPE); + const rule = store.add(makeRule('Jean', 'Pierre')); + expect(store.remove(rule.id)).toBe(true); + expect(store.getAll()).toHaveLength(0); + }); + + it('remove retourne false si la règle est introuvable', () => { + const store = new MappingStore(FILE_SCOPE); + expect(store.remove('inexistant')).toBe(false); + }); + + // --- Filtrage par portée --- + + it('getValidatedFor retourne les règles validées pour un fichier donné', () => { + const store = new MappingStore({ type: 'vault' }); + store.add(makeRule('Jean', 'Pierre')); + store.add({ ...makeRule('Marie', 'Sophie'), scope: { type: 'file', path: 'autre.cha' } }); + const results = store.getValidatedFor('entretien_01.srt'); + expect(results).toHaveLength(1); // seule la règle vault s'applique + expect(results[0].source).toBe('Jean'); + }); + + it('getValidatedFor exclut les règles non validées', () => { + const store = new MappingStore(FILE_SCOPE); + store.add({ ...makeRule('Jean', 'Pierre'), status: 'suggested' }); + expect(store.getValidatedFor('entretien_01.srt')).toHaveLength(0); + }); + + it('getValidatedFor inclut les règles de dossier parent', () => { + const store = new MappingStore({ type: 'vault' }); + store.add({ + ...makeRule('Jean', 'Pierre'), + scope: { type: 'folder', path: 'Transcriptions/UJAA' }, + }); + expect(store.getValidatedFor('Transcriptions/UJAA/entretien_01.cha')).toHaveLength(1); + expect(store.getValidatedFor('Transcriptions/LIIPPS/entretien_02.cha')).toHaveLength(0); + }); + + // --- Sérialisation JSON --- + + it('toJSON produit un MappingFile valide', () => { + const store = new MappingStore(FILE_SCOPE, 'UJAA-2026'); + store.add(makeRule('Jean', 'Pierre')); + const json = store.toJSON(); + expect(json.schemaVersion).toBe('1.0.0'); + expect(json.project).toBe('UJAA-2026'); + expect(json.mappings).toHaveLength(1); + expect(json.settings).toEqual(DEFAULT_FILE_SETTINGS); + }); + + it('fromJSON reconstruit un store identique', () => { + const store = new MappingStore(FILE_SCOPE, 'UJAA-2026'); + const rule = store.add(makeRule('Jean', 'Pierre')); + const json = store.toJSON(); + + const restored = MappingStore.fromJSON(json); + expect(restored.getAll()).toHaveLength(1); + expect(restored.get(rule.id)?.source).toBe('Jean'); + expect(restored.project).toBe('UJAA-2026'); + }); + + it('round-trip JSON : toJSON puis fromJSON puis toJSON est stable', () => { + const store = new MappingStore(FILE_SCOPE); + store.add(makeRule('Jean', 'Pierre')); + store.add(makeRule('Marie', 'Sophie')); + const json1 = store.toJSON(); + const json2 = MappingStore.fromJSON(json1).toJSON(); + expect(json2.mappings).toHaveLength(json1.mappings.length); + expect(json2.mappings.map((r) => r.source).sort()).toEqual( + json1.mappings.map((r) => r.source).sort() + ); + }); + + it('toJSON peut être sérialisé en JSON.stringify sans erreur', () => { + const store = new MappingStore(FILE_SCOPE); + store.add(makeRule('Jean', 'Pierre')); + const serialized = JSON.stringify(store.toJSON()); + const parsed: MappingFile = JSON.parse(serialized); + expect(parsed.mappings[0].source).toBe('Jean'); + }); +}); diff --git a/tests/unit/OccurrenceScanner.test.ts b/tests/unit/OccurrenceScanner.test.ts new file mode 100644 index 0000000..f32f659 --- /dev/null +++ b/tests/unit/OccurrenceScanner.test.ts @@ -0,0 +1,71 @@ +import { scanOccurrences } from '../../src/scanner/OccurrenceScanner'; +import type { MappingRule } from '../../src/types'; + +const FILE = 'entretien_01.md'; +const SCOPE = { type: 'file' as const, path: FILE }; + +function rule(id: string, source: string, replacement: string, priority = 0): MappingRule { + return { + id, source, replacement, category: 'first_name', scope: SCOPE, + status: 'validated', priority, createdBy: 'user', createdAt: '', + }; +} + +const TEXT = 'Bonjour Jean, tu habites Saint-Jean-de-Luz. Jean était là.'; +const RULES = [ + rule('1', 'Jean', 'Pierre', 0), + rule('2', 'Saint-Jean-de-Luz', 'Ville littorale', 0), +]; + +describe('OccurrenceScanner', () => { + it('trouve toutes les occurrences des sources', () => { + const occs = scanOccurrences(TEXT, FILE, RULES); + // Jean×3 + Saint-Jean-de-Luz×1 (dont Jean intérieur, whole-word = true donc Jean dans Saint-Jean-de-Luz est trouvé) + const sources = occs.map((o) => o.text); + expect(sources).toContain('Jean'); + expect(sources).toContain('Saint-Jean-de-Luz'); + }); + + it('retourne les occurrences triées par position', () => { + const occs = scanOccurrences(TEXT, FILE, RULES); + for (let i = 1; i < occs.length; i++) { + expect(occs[i].start).toBeGreaterThanOrEqual(occs[i - 1].start); + } + }); + + it('associe chaque occurrence à son mappingId', () => { + const occs = scanOccurrences(TEXT, FILE, RULES); + const jeanOcc = occs.find((o) => o.text === 'Jean' && o.start === 8); + expect(jeanOcc?.mappingId).toBe('1'); + const villOcc = occs.find((o) => o.text === 'Saint-Jean-de-Luz'); + expect(villOcc?.mappingId).toBe('2'); + }); + + it('remplit contextBefore et contextAfter', () => { + const occs = scanOccurrences(TEXT, FILE, RULES); + const first = occs[0]; + expect(first.contextBefore).toContain('Bonjour'); + expect(first.contextAfter.length).toBeGreaterThan(0); + }); + + it('calcule le numéro de ligne (base 1)', () => { + const multiline = 'ligne un\nBonjour Jean ici\nligne trois'; + const occs = scanOccurrences(multiline, FILE, [rule('1', 'Jean', 'Pierre')]); + expect(occs[0].line).toBe(2); + }); + + it('retourne un tableau vide si aucune règle', () => { + expect(scanOccurrences(TEXT, FILE, [])).toHaveLength(0); + }); + + it('retourne un tableau vide si aucun terme trouvé', () => { + const occs = scanOccurrences('Aucun terme ici.', FILE, RULES); + expect(occs).toHaveLength(0); + }); + + it('chaque occurrence a un id unique', () => { + const occs = scanOccurrences(TEXT, FILE, RULES); + const ids = occs.map((o) => o.id); + expect(new Set(ids).size).toBe(ids.length); + }); +}); diff --git a/tests/unit/PseudonymizationEngine.test.ts b/tests/unit/PseudonymizationEngine.test.ts new file mode 100644 index 0000000..85a59d7 --- /dev/null +++ b/tests/unit/PseudonymizationEngine.test.ts @@ -0,0 +1,215 @@ +import { PseudonymizationEngine } from '../../src/pseudonymizer/PseudonymizationEngine'; +import { sortRules, findSpansForRule, buildReplacementPlan } from '../../src/pseudonymizer/ReplacementPlanner'; +import { resolveSpans, applySpans } from '../../src/pseudonymizer/SpanProtector'; +import type { MappingRule } from '../../src/types'; + +const FILE_SCOPE = { type: 'file' as const, path: 'entretien_01.srt' }; + +function rule( + id: string, + source: string, + replacement: string, + priority = 0 +): MappingRule { + return { + id, + source, + replacement, + category: 'first_name', + scope: FILE_SCOPE, + status: 'validated', + priority, + createdBy: 'user', + createdAt: new Date().toISOString(), + }; +} + +const engine = new PseudonymizationEngine(); + +// --- SPECS §17.1 : remplacement simple --- + +describe('Remplacement simple (SPECS §17.1)', () => { + it('Jean → Pierre dans une phrase courte', () => { + expect(engine.pseudonymize('Bonjour Jean.', [rule('1', 'Jean', 'Pierre')])).toBe( + 'Bonjour Pierre.' + ); + }); + + it('remplace toutes les occurrences dans le texte', () => { + const result = engine.pseudonymize('Jean parle à Jean.', [rule('1', 'Jean', 'Pierre')]); + expect(result).toBe('Pierre parle à Pierre.'); + }); + + it("n'applique pas les règles au statut 'suggested'", () => { + const suggested: MappingRule = { ...rule('1', 'Jean', 'Pierre'), status: 'suggested' }; + expect(engine.pseudonymize('Bonjour Jean.', [suggested])).toBe('Bonjour Jean.'); + }); +}); + +// --- SPECS §17.2 : remplacement composé prioritaire --- + +describe('Remplacement composé — jamais Saint-Pierre-de-Luz (SPECS §17.2)', () => { + const rules = [ + rule('1', 'Jean', 'Pierre', 0), + rule('2', 'Saint-Jean-de-Luz', 'Ville moyenne limitrophe', 0), + ]; + + it('produit Pierre habite Ville moyenne limitrophe.', () => { + const result = engine.pseudonymize('Jean habite Saint-Jean-de-Luz.', rules); + expect(result).toBe('Pierre habite Ville moyenne limitrophe.'); + }); + + it('ne produit jamais Saint-Pierre-de-Luz', () => { + const result = engine.pseudonymize('Jean habite Saint-Jean-de-Luz.', rules); + expect(result).not.toContain('Saint-Pierre-de-Luz'); + }); + + it("Jean seul (hors contexte composé) est bien remplacé par Pierre", () => { + const result = engine.pseudonymize('Jean habite Saint-Jean-de-Luz.', rules); + expect(result.startsWith('Pierre')).toBe(true); + }); +}); + +// --- SPECS §18.2 : tests de non-régression --- + +describe('Non-régression (SPECS §18.2)', () => { + it('Paul / Saint-Paul : Saint-Paul prime sur Paul', () => { + const rules = [rule('1', 'Paul', 'Marc', 0), rule('2', 'Saint-Paul', 'Commune rurale', 0)]; + const result = engine.pseudonymize('Paul habite Saint-Paul.', rules); + expect(result).toBe('Marc habite Commune rurale.'); + expect(result).not.toContain('Saint-Marc'); + }); + + it('Montpellier / CHU de Montpellier : entité longue prime', () => { + const rules = [ + rule('1', 'Montpellier', 'Métropole du Sud', 0), + rule('2', 'CHU de Montpellier', 'CHU régional', 0), + ]; + const result = engine.pseudonymize( + 'Elle travaille au CHU de Montpellier à Montpellier.', + rules + ); + expect(result).toBe('Elle travaille au CHU régional à Métropole du Sud.'); + expect(result).not.toContain('CHU de Métropole'); + }); + + it('Marie / Sainte-Marie : Sainte-Marie prime sur Marie', () => { + const rules = [rule('1', 'Marie', 'Sophie', 0), rule('2', 'Sainte-Marie', 'Commune côtière', 0)]; + const result = engine.pseudonymize('Marie vient de Sainte-Marie.', rules); + expect(result).toBe('Sophie vient de Commune côtière.'); + expect(result).not.toContain('Sainte-Sophie'); + }); + + it('Luz / Saint-Jean-de-Luz : Saint-Jean-de-Luz prime sur Luz', () => { + const rules = [ + rule('1', 'Luz', 'Ville X', 0), + rule('2', 'Saint-Jean-de-Luz', 'Ville moyenne limitrophe', 0), + ]; + const result = engine.pseudonymize('Il vient de Saint-Jean-de-Luz.', rules); + expect(result).toBe('Il vient de Ville moyenne limitrophe.'); + expect(result).not.toContain('Saint-Jean-de-Ville X'); + }); +}); + +// --- Comportement du z-index (priorité manuelle) --- + +describe('Priorité z-index', () => { + it('une règle courte avec priority élevée passe avant une règle longue à priority 0', () => { + // L'utilisateur a explicitement choisi de traiter Jean avant l'entité composée + const rules = [rule('1', 'Jean', 'Pierre', 100), rule('2', 'Saint-Jean-de-Luz', 'Ville X', 0)]; + const result = engine.pseudonymize('Jean habite Saint-Jean-de-Luz.', rules); + // Jean [0,4] priority 100 → accepté en premier + // Jean [18,22] inside Saint-Jean-de-Luz → accepté (priority 100, avant [12,29] priority 0) + // Saint-Jean-de-Luz chevauche avec Jean [18,22] → rejeté + // Résultat : Pierre habite Saint-Pierre-de-Luz. (comportement attendu quand l'utilisateur choisit ce z-index) + expect(result).toContain('Pierre'); + expect(result).not.toContain('Jean'); + }); + + it('deux règles à même priority : la plus longue source prime', () => { + const rules = [rule('1', 'Marie', 'Sophie', 5), rule('2', 'Sainte-Marie', 'Commune', 5)]; + const result = engine.pseudonymize('Marie vient de Sainte-Marie.', rules); + expect(result).toBe('Sophie vient de Commune.'); + expect(result).not.toContain('Sainte-Sophie'); + }); +}); + +// --- Whole-word matching --- + +describe('Whole-word matching', () => { + it('Jean ne remplace pas Jeannine', () => { + const result = engine.pseudonymize('Bonjour Jeannine.', [rule('1', 'Jean', 'Pierre')]); + expect(result).toBe('Bonjour Jeannine.'); + }); + + it('Jean ne remplace pas Jean-Baptiste', () => { + const result = engine.pseudonymize('Bonjour Jean-Baptiste.', [rule('1', 'Jean', 'Pierre')]); + // Jean est séparé par - (non alphanumérique) → considéré whole-word → remplacé + // C'est le comportement attendu : Jean-Baptiste avec Jean comme prénom → Pierre-Baptiste + expect(result).toBe('Bonjour Pierre-Baptiste.'); + }); + + it('Marie ne remplace pas Mariette', () => { + const result = engine.pseudonymize('Bonjour Mariette.', [rule('1', 'Marie', 'Sophie')]); + expect(result).toBe('Bonjour Mariette.'); + }); +}); + +// --- ReplacementPlanner unitaire --- + +describe('ReplacementPlanner.sortRules', () => { + it('trie par priority décroissante', () => { + const rules = [rule('1', 'Jean', 'Pierre', 0), rule('2', 'Marie', 'Sophie', 10)]; + const sorted = sortRules(rules); + expect(sorted[0].source).toBe('Marie'); + }); + + it('à priority égale, trie par longueur source décroissante', () => { + const rules = [rule('1', 'Jean', 'Pierre', 0), rule('2', 'Saint-Jean-de-Luz', 'X', 0)]; + const sorted = sortRules(rules); + expect(sorted[0].source).toBe('Saint-Jean-de-Luz'); + }); +}); + +describe('ReplacementPlanner.findSpansForRule', () => { + const r = rule('1', 'jean', 'Pierre', 0); + + it('trouve les occurrences en mode case-insensitive (défaut)', () => { + const spans = findSpansForRule('Bonjour Jean et jean.', r, { + caseSensitive: false, + wholeWordOnly: false, + }); + expect(spans).toHaveLength(2); + }); + + it('whole-word : ne trouve pas jean dans Jeannine', () => { + const spans = findSpansForRule('Bonjour Jeannine.', r, { + caseSensitive: false, + wholeWordOnly: true, + }); + expect(spans).toHaveLength(0); + }); +}); + +// --- SpanProtector unitaire --- + +describe('SpanProtector', () => { + it('resolveSpans élimine un span court qui chevauche un span long', () => { + const spans = [ + { start: 0, end: 4, source: 'Jean', replacement: 'Pierre', mappingId: '1', priority: 0 }, + { start: 0, end: 17, source: 'Jean Dupont', replacement: 'Sophie Arnaud', mappingId: '2', priority: 0 }, + ]; + const resolved = resolveSpans(spans); + expect(resolved).toHaveLength(1); + expect(resolved[0].source).toBe('Jean Dupont'); + }); + + it('applySpans applique de droite à gauche sans décaler les indices', () => { + const text = 'Jean habite Lyon.'; + const spans = resolveSpans([ + { start: 0, end: 4, source: 'Jean', replacement: 'Pierre', mappingId: '1', priority: 0 }, + { start: 12, end: 16, source: 'Lyon', replacement: 'Grenoble', mappingId: '2', priority: 0 }, + ]); + expect(applySpans(text, spans)).toBe('Pierre habite Grenoble.'); + }); +}); diff --git a/tests/unit/SrtParser.test.ts b/tests/unit/SrtParser.test.ts new file mode 100644 index 0000000..073c6d7 --- /dev/null +++ b/tests/unit/SrtParser.test.ts @@ -0,0 +1,111 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { SrtParser } from '../../src/parsers/SrtParser'; + +const FIXTURE = path.join(__dirname, '../fixtures/entretien_01.srt'); + +describe('SrtParser', () => { + const parser = new SrtParser(); + let content: string; + + beforeAll(() => { + content = fs.readFileSync(FIXTURE, 'utf-8'); + }); + + // --- Structure --- + + it('parse le bon nombre de blocs', () => { + const doc = parser.parse(content); + expect(doc.blocks).toHaveLength(6); + }); + + it('extrait index, timestamps et texte du bloc 1', () => { + const { blocks } = parser.parse(content); + expect(blocks[0].index).toBe(1); + expect(blocks[0].startTime).toBe('00:00:01,000'); + expect(blocks[0].endTime).toBe('00:00:04,500'); + expect(blocks[0].lines).toEqual(['Bonjour Jean, tu peux te présenter ?']); + }); + + it('extrait correctement le bloc 2 (Jean Dupont / Saint-Jean-de-Luz)', () => { + const { blocks } = parser.parse(content); + expect(blocks[1].index).toBe(2); + expect(blocks[1].startTime).toBe('00:00:05,000'); + expect(blocks[1].endTime).toBe('00:00:09,200'); + expect(blocks[1].lines[0]).toContain('Jean Dupont'); + expect(blocks[1].lines[0]).toContain('Saint-Jean-de-Luz'); + }); + + it('les blocs sont numérotés séquentiellement', () => { + const { blocks } = parser.parse(content); + blocks.forEach((b, i) => expect(b.index).toBe(i + 1)); + }); + + // --- Round-trip (SPECS §17.4) --- + + it('round-trip exact : reconstruct(parse(content)) === content', () => { + const doc = parser.parse(content); + expect(parser.reconstruct(doc)).toBe(content); + }); + + it('les timestamps sont inchangés après reconstruction', () => { + const doc = parser.parse(content); + const lines = parser.reconstruct(doc).split('\n'); + expect(lines[1]).toBe('00:00:01,000 --> 00:00:04,500'); + expect(lines[5]).toBe('00:00:05,000 --> 00:00:09,200'); + }); + + it('les numéros de blocs sont inchangés après reconstruction', () => { + const doc = parser.parse(content); + const lines = parser.reconstruct(doc).split('\n'); + expect(lines[0]).toBe('1'); + expect(lines[4]).toBe('2'); + }); + + // --- Remplacement de texte sans toucher la structure --- + + it("modifier le texte d'un bloc ne touche pas son timestamp", () => { + const doc = parser.parse(content); + doc.blocks[0].lines[0] = 'Bonjour Pierre, tu peux te présenter ?'; + const lines = parser.reconstruct(doc).split('\n'); + expect(lines[1]).toBe('00:00:01,000 --> 00:00:04,500'); + expect(lines[2]).toBe('Bonjour Pierre, tu peux te présenter ?'); + }); + + it('remplacer Jean par Pierre dans le bloc 2 préserve Saint-Jean-de-Luz', () => { + const doc = parser.parse(content); + // Remplacement correct (entité longue d'abord) : Saint-Jean-de-Luz → Ville littorale, + // puis Jean → Pierre sur les occurrences restantes + doc.blocks[1].lines[0] = doc.blocks[1].lines[0] + .replace('Saint-Jean-de-Luz', 'Ville littorale') + .replace('Jean Dupont', 'Pierre Martin'); + const result = parser.reconstruct(doc); + expect(result).not.toContain('Saint-Pierre-de-Luz'); + expect(result).toContain('Ville littorale'); + expect(result).toContain('Pierre Martin'); + }); + + // --- Robustesse --- + + it('parse un SRT avec fins de ligne Windows (CRLF)', () => { + const crlf = content.replace(/\n/g, '\r\n'); + const doc = parser.parse(crlf); + expect(doc.blocks).toHaveLength(6); + expect(doc.blocks[0].startTime).toBe('00:00:01,000'); + expect(doc.blocks[0].endTime).toBe('00:00:04,500'); + }); + + it('parse un SRT avec plusieurs lignes vides entre blocs', () => { + const extraBlank = content.replace(/\n\n/g, '\n\n\n'); + const doc = parser.parse(extraBlank); + expect(doc.blocks).toHaveLength(6); + }); + + it('parse un SRT sans saut de ligne final', () => { + const noTrailing = content.trimEnd(); + const doc = parser.parse(noTrailing); + expect(doc.trailingNewline).toBe(false); + expect(doc.blocks).toHaveLength(6); + expect(parser.reconstruct(doc)).toBe(noTrailing); + }); +}); diff --git a/tests/unit/TranscriptConverter.test.ts b/tests/unit/TranscriptConverter.test.ts new file mode 100644 index 0000000..62b9869 --- /dev/null +++ b/tests/unit/TranscriptConverter.test.ts @@ -0,0 +1,93 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { SrtParser } from '../../src/parsers/SrtParser'; +import { ChatParser } from '../../src/parsers/ChatParser'; +import { srtToMarkdown, chatToMarkdown } from '../../src/parsers/TranscriptConverter'; + +const SRT_FIXTURE = path.join(__dirname, '../fixtures/entretien_01.srt'); +const CHA_FIXTURE = path.join(__dirname, '../fixtures/entretien_02.cha'); + +describe('srtToMarkdown', () => { + let md: string; + + beforeAll(() => { + const content = fs.readFileSync(SRT_FIXTURE, 'utf-8'); + const doc = new SrtParser().parse(content); + md = srtToMarkdown(doc, 'entretien_01.srt'); + }); + + it('produit un frontmatter avec le format et la source', () => { + expect(md).toContain('pseudobs-format: srt'); + expect(md).toContain('pseudobs-source: "entretien_01.srt"'); + }); + + it('contient le texte de chaque bloc SRT', () => { + expect(md).toContain('Bonjour Jean, tu peux te présenter ?'); + expect(md).toContain("Saint-Jean-de-Luz"); + expect(md).toContain('CHU de Montpellier'); + }); + + it('affiche les timestamps en italique', () => { + expect(md).toContain('*00:00:01,000 → 00:00:04,500*'); + expect(md).toContain('*00:00:05,000 → 00:00:09,200*'); + }); + + it('affiche les numéros de blocs en gras entre crochets', () => { + expect(md).toContain('**[1]**'); + expect(md).toContain('**[6]**'); + }); + + it('ne contient pas de syntaxe SRT brute (-->) en dehors du frontmatter', () => { + const body = md.split('---').slice(2).join('---'); + expect(body).not.toContain('-->'); + }); + + it('termine par un saut de ligne', () => { + expect(md.endsWith('\n')).toBe(true); + }); +}); + +describe('chatToMarkdown', () => { + let md: string; + + beforeAll(() => { + const content = fs.readFileSync(CHA_FIXTURE, 'utf-8'); + const doc = new ChatParser().parse(content); + md = chatToMarkdown(doc, 'entretien_02.cha'); + }); + + it('produit un frontmatter avec le format et la source', () => { + expect(md).toContain('pseudobs-format: chat'); + expect(md).toContain('pseudobs-source: "entretien_02.cha"'); + }); + + it('convertit les tours de parole en **SPEAKER** : texte', () => { + expect(md).toContain('**INV** : bonjour Marie, tu peux te présenter ?'); + expect(md).toContain('**PAR** : je m\'appelle Marie Dupont'); + }); + + it('convertit les lignes @ en blockquote', () => { + expect(md).toContain('> @Begin'); + expect(md).toContain('> @Languages: fra'); + expect(md).toContain('> @End'); + }); + + it('convertit les lignes % en blockquote', () => { + expect(md).toContain('> %com: PAR marque une hésitation'); + }); + + it('contient le cas Nancy ville/prenom dans un tour PAR', () => { + expect(md).toContain('**PAR** : oui (0.5) à Nancy'); + expect(md).toContain('la ville'); + expect(md).toContain('le prénom'); + }); + + it('ne contient pas de syntaxe CHA brute (*SPEAKER:) en dehors du frontmatter', () => { + const body = md.split('---').slice(2).join('---'); + expect(body).not.toMatch(/^\*[A-Z]+:/m); + }); + + it('termine par un saut de ligne', () => { + expect(md.endsWith('\n')).toBe(true); + }); +}); diff --git a/tests/unit/types.test.ts b/tests/unit/types.test.ts new file mode 100644 index 0000000..1e956e3 --- /dev/null +++ b/tests/unit/types.test.ts @@ -0,0 +1,125 @@ +import type { + MappingRule, + MappingStatus, + EntityCategory, + ScopeType, + Occurrence, + ReplacementSpan, + DictionaryEntry, + MappingFile, +} from '../../src/types'; + +// Smoke test Phase 0 : les types sont cohérents et les valeurs par défaut correctes + +describe('Types partagés', () => { + it('MappingRule accepte priority = 0 par défaut (z-index)', () => { + const rule: MappingRule = { + id: 'map_000001', + source: 'Jean', + replacement: 'Pierre', + category: 'first_name', + scope: { type: 'file', path: 'entretien_01.srt' }, + status: 'validated', + priority: 0, + createdBy: 'user', + createdAt: new Date().toISOString(), + }; + expect(rule.priority).toBe(0); + expect(rule.source).toBe('Jean'); + }); + + it('MappingRule avec priority élevée passe avant priority basse', () => { + const rules: MappingRule[] = [ + { id: '1', source: 'Jean', replacement: 'Pierre', category: 'first_name', + scope: { type: 'file' }, status: 'validated', priority: 0, + createdBy: 'user', createdAt: '' }, + { id: '2', source: 'Saint-Jean-de-Luz', replacement: 'Ville moyenne littorale', + category: 'place', scope: { type: 'file' }, status: 'validated', priority: 10, + createdBy: 'user', createdAt: '' }, + ]; + const sorted = [...rules].sort((a, b) => b.priority - a.priority); + expect(sorted[0].source).toBe('Saint-Jean-de-Luz'); + }); + + it('ScopeType couvre les trois portées', () => { + const portées: ScopeType[] = ['file', 'folder', 'vault']; + expect(portées).toHaveLength(3); + }); + + it('MappingStatus couvre tous les statuts définis dans SPECS §5.4', () => { + const statuts: MappingStatus[] = [ + 'suggested', 'validated', 'ignored', 'partial', + 'conflict', 'disabled', 'needs_review', + ]; + expect(statuts).toHaveLength(7); + }); + + it('EntityCategory couvre toutes les catégories', () => { + const catégories: EntityCategory[] = [ + 'first_name', 'last_name', 'full_name', 'place', + 'institution', 'date', 'age', 'profession', 'custom', + ]; + expect(catégories).toHaveLength(9); + }); + + it('ReplacementSpan positionne correctement un remplacement', () => { + const span: ReplacementSpan = { + start: 8, + end: 12, + source: 'Jean', + replacement: 'Pierre', + mappingId: 'map_000001', + priority: 0, + }; + expect(span.end - span.start).toBe(span.source.length); + }); + + it('DictionaryEntry Coulmont contient les métadonnées sociologiques', () => { + const entry: DictionaryEntry = { + value: 'Thibault', + type: 'first_name', + gender: 'masculine', + decade: 1989, + socialClass: 'supérieur', + origin: 'fr', + replacementCandidates: ['Édouard', 'Gauthier', 'Clément'], + }; + expect(entry.socialClass).toBe('supérieur'); + expect(entry.decade).toBe(1989); + }); + + it('MappingFile respecte le schéma SPECS §5.2', () => { + const table: MappingFile = { + schemaVersion: '1.0.0', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + project: 'UJAA-2026', + scope: { type: 'folder', path: 'Transcriptions/UJAA' }, + mappings: [], + }; + expect(table.schemaVersion).toBe('1.0.0'); + expect(table.scope.type).toBe('folder'); + }); + + it('cas non-régression : Jean ne doit jamais remplacer Saint-Jean-de-Luz avant lui', () => { + // Le span le plus long avec priority >= span court doit être appliqué en premier + const spans: ReplacementSpan[] = [ + { start: 14, end: 31, source: 'Saint-Jean-de-Luz', replacement: 'Ville moyenne littorale', mappingId: '2', priority: 10 }, + { start: 21, end: 25, source: 'Jean', replacement: 'Pierre', mappingId: '1', priority: 0 }, + ]; + // Après résolution, seul le span englobant doit être retenu + const sorted = [...spans].sort((a, b) => { + if (b.priority !== a.priority) return b.priority - a.priority; + return (b.end - b.start) - (a.end - a.start); + }); + const accepted: ReplacementSpan[] = []; + for (const candidate of sorted) { + const overlaps = accepted.some( + (s) => candidate.start < s.end && candidate.end > s.start + ); + if (!overlaps) accepted.push(candidate); + } + expect(accepted).toHaveLength(1); + expect(accepted[0].source).toBe('Saint-Jean-de-Luz'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a5a9cc7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES2018", + "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "lib": ["ES2018", "DOM"] + }, + "include": ["src/**/*.ts"] +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..c074a1c --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "0.0.1": "1.4.0" +}