Compare commits

...

32 commits

Author SHA1 Message Date
Mara-Li
be224777ff
chore: typo 2026-05-16 10:16:26 +02:00
Lisandra
4199f7ca54
Update README.md 2025-02-27 16:37:05 +01:00
Mara-Li
e2185fa767 chore(release): 0.0.10 2025-02-27 12:23:44 +01:00
Mara-Li
99f274cbe2 docs(translation): avoid settings in settings heading 2025-02-27 12:23:29 +01:00
Mara-Li
714a8ed17d refactor: remove useless logs 2025-02-27 12:22:39 +01:00
Mara-Li
336bb21b17 chore(release): 0.0.9 2025-02-24 13:16:33 +01:00
Mara-Li
04d40100e5 feat(settings): add excludeTermKey setting to filter tags in frontmatter
- add settings to set the name for the frontmatter key (by default, `exclude_term`)
- add setting to allow auto-clean existing tag (so it will remove the tags if it was excluded)
- update function to add the filter
- refactor settings to be more readable (style)
- refactor function (moving to utils)
- prevent empty message if missing tags is empty
2025-02-24 13:15:00 +01:00
Mara-Li
a9bc93800f chore(release): 0.0.8 2025-02-21 22:55:53 +01:00
Mara-Li
25c6fab086 feat(utils): add array comparison and missing elements functions for tag management 2025-02-21 22:55:47 +01:00
Mara-Li
9ec3bb32c6 docs(readme): clarify thesaurus format to require at least two columns 2025-02-21 22:19:44 +01:00
Mara-Li
5abff9cf66 fix(csv): enhance CSV parsing with separator regex and column count validation 2025-02-21 22:19:40 +01:00
Mara-Li
86fcbc23d0 feat(tests): add test for error handling with insufficient columns in CSV 2025-02-21 22:19:28 +01:00
Mara-Li
df3f1c6dd6 feat(csv): allow more than 2 columns 2025-02-21 22:05:48 +01:00
Mara-Li
1b50050700 feat(tests): add constants for mock translations and enhance CSV parsing tests 2025-02-21 22:05:28 +01:00
Mara-Li
4036c76e57 chore(release): 0.0.7 2025-02-21 10:05:07 +01:00
Mara-Li
6d360588e6 docs: update descriptions in JSON and README for clarity on thesaurus settings 2025-02-21 10:04:47 +01:00
Mara-Li
cdbfe6750b chore(release): 0.0.6 2025-02-21 08:41:05 +01:00
Mara-Li
61e0159615 style(lint): run biome 2025-02-21 08:37:19 +01:00
Mara-Li
dc2e2853e0 test: add tests for accent handling in thesaurus and tag retrieval 2025-02-21 08:35:10 +01:00
Mara-Li
9af25f93eb refactor(settings): remove accents during test for a good thésaurus 2025-02-21 08:35:04 +01:00
Mara-Li
59d663b877 docs: update README to clarify accent sensitivity and search behavior 2025-02-21 08:34:41 +01:00
Mara-Li
25e7c38de2 feat(standardize): allow to remove accents when reading the table 2025-02-21 08:19:31 +01:00
Mara-Li
ada642f7ee chore(release): 0.0.5 2025-02-18 22:35:39 +01:00
Mara-Li
aa3b8ef598 fix: tags & tag lost
prevent duplication if "tag" is used in place of tags (old obsidian version)
2025-02-18 22:35:17 +01:00
Lisandra
087b88cb2c
Docs: add credit to MyThesaurus python 2025-02-10 23:13:01 +01:00
Mara
43a7702359 chore(release): 0.0.4 2025-02-09 18:28:07 +01:00
Mara
2cfd04a50e test: reformat 2025-02-09 18:27:56 +01:00
Mara
55a180c79d docs: add thésaurus table in README 2025-02-09 18:27:49 +01:00
Mara
77b02f0462 fix: throw error when the path to the thesaurus is empty 2025-02-09 16:28:35 +01:00
Mara
1d6e018e30 chore: adjust wording 2025-02-08 22:59:32 +01:00
Mara
465ca9e429 chore(release): 0.0.3 2025-02-08 22:59:13 +01:00
Mara
c253dafb0e feat: allow markdown table 2025-02-08 22:58:34 +01:00
20 changed files with 809 additions and 268 deletions

View file

@ -1,3 +1,44 @@
## [0.0.10](https://github.com/Mara-Li/obsidian-my-thesaurus/compare/0.0.9...0.0.10) (2025-02-27)
## [0.0.9](https://github.com/Mara-Li/obsidian-my-thesaurus/compare/0.0.8...0.0.9) (2025-02-24)
### Features
* **settings:** add excludeTermKey setting to filter tags in frontmatter ([04d4010](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/04d40100e56ca4c3eccb4b3153c2f48424b72cf0))
## [0.0.8](https://github.com/Mara-Li/obsidian-my-thesaurus/compare/0.0.7...0.0.8) (2025-02-21)
### Features
* **csv:** allow more than 2 columns ([df3f1c6](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/df3f1c6dd625fcb508a7c360e8f5c5d853839f44))
* **tests:** add constants for mock translations and enhance CSV parsing tests ([1b50050](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/1b50050700d386a35061d79c119cafb77ed04b87))
* **tests:** add test for error handling with insufficient columns in CSV ([86fcbc2](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/86fcbc23d02697177c8e0afa2676859be4826b55))
* **utils:** add array comparison and missing elements functions for tag management ([25c6fab](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/25c6fab0865458f45d8ce4e079f1dc9cbe601001))
### Bug Fixes
* **csv:** enhance CSV parsing with separator regex and column count validation ([5abff9c](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/5abff9cf66edfb3122d568fe97d08a7b51d653bb))
## [0.0.7](https://github.com/Mara-Li/obsidian-my-thesaurus/compare/0.0.6...0.0.7) (2025-02-21)
## [0.0.6](https://github.com/Mara-Li/obsidian-my-thesaurus/compare/0.0.5...0.0.6) (2025-02-21)
### Features
* **standardize:** allow to remove accents when reading the table ([25e7c38](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/25e7c38de2662b5206bafa18dbc522de6c3d5c97))
## [0.0.5](https://github.com/Mara-Li/obsidian-my-thesaurus/compare/0.0.4...0.0.5) (2025-02-18)
### Bug Fixes
* tags & tag lost ([aa3b8ef](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/aa3b8ef598ce977eaa3e015b72cee07572c6a3d3))
## [0.0.4](https://github.com/Mara-Li/obsidian-my-thesaurus/compare/0.0.3...0.0.4) (2025-02-09)
### Bug Fixes
* throw error when the path to the thesaurus is empty ([77b02f0](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/77b02f04620bc8e88666146e84a85fb662c6a181))
## [0.0.3](https://github.com/Mara-Li/obsidian-my-thesaurus/compare/0.0.2...0.0.3) (2025-02-08)
### Features
* allow markdown table ([c253daf](https://github.com/Mara-Li/obsidian-my-thesaurus/commit/c253dafb0e525eff4e1a5627baae010c37f7c67d))
## [0.0.2](https://github.com/Mara-Li/obsidian-my-thesaurus/compare/0.0.1...0.0.2) (2025-02-08)
## 0.0.1 (2025-02-08)

View file

@ -1,22 +1,64 @@
# My Thesaurus
Ajoute automatiquement des tags en fonction dun thésaurus au format CSV.
Ajoute automatiquement des tags en fonction dun thésaurus au format CSV ou d'une table Markdown.
Le plugin cherche le mot ou la suite de mots dans l'entête (propriétés), dans le corps et le titre de la note. Le mot recherché le sera :
- Strictement (sans troncature à droite ou gauche),
- Insensible à la casse (majuscule ou minuscules)
- Sensible aux accents (paramétrable)
> [!TIP]
> `jeu` trouve `JEU` ou `JEu`
> `jeu de carte` trouve `Jeu De Carte` mais pas `jeux de cartes`
> `jeu` ne trouvera ni `jeux`, `enjeu` ou `rajeunir`
> `rôle` ne trouvera pas `role`
> `role` ne trouvera pas `rôle`
## 📚 Format du Thésaurus
Le thésaurus doit être un tableau avec au moins deux colonnes : une pour les termes et une pour les synonymes. La première ligne doit contenir les en-têtes de colonnes.
> [!TIP]
> Le thésaurus peut être un fichier CSV comme ceci :
> ```csv
> Terme,Synonyme
> ordinateur,PC
> ordinateur,machine
> ordinateur,vélo
> ```
>
> Ou en format Markdown :
> ```markdown
> | Terme | Synonyme |
> |------------|---------------|
> | ordinateur | PC |
> | ordinateur | machine |
> | ordinateur | processeur |
> ```
> [!NOTE]
> Si vous choisissez d'utiliser un fichier Markdown, la table Markdown doit être le seul contenu du fichier.
> Cependant, vous pouvez avoir un entête YAML (propriété) au début du fichier.
## ⚙️ Paramètres
1. <ins>Séparateur</ins> : Le séparateur utilisé dans le fichier CSV. Les séparateurs pris en charge sont `,`, `;`, `|` et `tabulation`.
2. <ins>Colonne cible</ins> : Le titre de la colonne du fichier CSV contenant les tags :
- **Terme** : La colonne contient les tags qui doivent être ajoutés par le plugin.
- **Synonymes** : La colonne contient les synonymes qui doivent être reconnus par le plugin.
3. <ins>Thésaurus</ins> : Chemin du fichier contenant le thésaurus. Ce fichier doit être un CSV et être stocké dans votre coffre (vault). Vous pouvez cliquer sur le bouton `save` pour vérifier que le fichier est valide (chemin correct, fichier existant et bien formé).
1. <ins>Thésaurus</ins> : Chemin du fichier contenant le thésaurus. Ce fichier doit être un CSV et être stocké dans votre coffre (vault). Vous pouvez cliquer sur le bouton `save` pour vérifier que le fichier est valide (chemin correct, fichier existant et bien formé).
> [!WARNING]
> Si, pour une raison quelconque, le fichier est déplacé, vous devrez mettre à jour son chemin dans les paramètres ! Il ne sera pas mis à jour automatiquement.
2. <ins>Séparateur</ins> : Le séparateur utilisé dans le fichier CSV. Les séparateurs pris en charge sont `,`, `;`, `|` et `tabulation`.
3. <ins>Colonne cible</ins> : Le titre de la colonne du fichier CSV contenant les tags :
- **Terme** : Nom de la colonne contenant les **tags à ajouter**.
- **Synonymes** : Nom de la colonne des mots à **rechercher** dans le fichier (contenu et titre).
4. <ins>Chemins inclus</ins> : Les fichiers situés dans ces dossiers seront pris en compte lors de l'exécution de la commande `My Thesaurus: Analyser tous les fichiers`. Vous pouvez séparer les chemins par une virgule, un point-virgule ou un saut de ligne. De plus, les expressions régulières (regex) sont prises en charge.
> [!TIP]
> Vous pouvez inclure tous les fichiers de votre coffre en utilisant `.*` comme chemin !
> Vous pouvez inclure tous les fichiers de votre coffre en utilisant `.*` comme chemin ! Cette valeur est utilisés par défaut.
5. <ins>Exclure des termes</ins> : Permet d'exclure des termes en utilisant une clé de propriété.
- **Clé de propriété** : La clé à rechercher dans les propriétés, par défaut `exclude_term`.
- **Auto-nettoyage** : Si activé, le plugin nettoiera les tags déjà existants en supprimant les termes exclus.
6. <ins>Supprimer les accents</ins> : Si activé, les accents seront supprimés lors de la reconnaissance de synonymes. Permet de faire correspondre `rôle` à `role` (et vice-versa).
## 📝 Utilisation

View file

@ -1,54 +1,100 @@
# My Thesaurus
# My Thesaurus
-> [French README](./README.fr.md)
Automagically adds tags based on a thesaurus CSV.
Automagically adds tags based on a thesaurus CSV or a Markdown table.
-> [French version](./README.fr.md)
The plugin searches for the word or series of words in the header (properties), body and title of the note. The word searched for will be :
- Strictly (without right or left truncation),
- Case-insensitive (upper or lower case)
- Accent-sensitive (configurable)
## ⚙️ Settings
> [!TIP]
> - `game` finds `GAME` or `GamE`.
> - `card game` finds `Card Game` but not `cards games`
> - `game` won't find `games`, `gaming` or `gameplay`
> - `cafe` will not find `café`.
> - `café` will not find `cafe`.
1. <ins>Separator</ins>: The separator used in the CSV file. Supported separators are `,`, `;`, `|`, and `tabulation`.
2. <ins>Column title</ins>: The title of the column in the CSV file that contains the tags:
- **Term**: The column contains the tags that must be set by the plugin.
- **Synonyms**: The column contains the synonyms that must be recognized by the plugin.
3. <ins>Thesaurus</ins>: Path to the file containing the thesaurus. The file must be a CSV file and must be in your vault. You can click on the `save` button to ensure the file is valid (the path is correct, the file exists, and it's well-formed).
> [!WARNING]
> If, for some reason, the file is moved, you need to update the path in the settings! It won't be updated automatically.
## 📚 Thesaurus Format
5. <u>Included paths</u>: Files in these folders will be included when using the command `My Thesaurus: Parse all files`. You can separate the paths with a comma, semicolon, or newline. Moreover, regex is supported.
The thesaurus must be a table with at last two columns: one for the terms and one for the synonyms. The first row must contain the column headers.
> [!TIP]
> You can include all the files in your vault by using `.*` as the path!
> The thesaurus can be a CSV file like this:
> ```csv
> Tag,Synonym
> computer,PC
> computer,machine
> computer,bike
> ```
>
> Or in Markdown format:
> ```markdown
> | Tag | Synonym |
> |-----------|-----------|
> | computer | PC |
> | computer | machine |
> | computer | processor |
> ```
## 📝 Usage
> [!NOTE]
> If you choose to use a markdown file, the Markdown table must be the only content in the file.
> But, you can have a frontmatter at the beginning of the file.
The plugin has two commands:
You can have as many synonyms as you want for a term, but you need to duplicate the term in the first column. Moreover, a synonym can be associated with multiple terms, and the synonym can equal to the term itself.
1. <ins>My Thesaurus: Parse all files</ins>: This command will parse all the files in the included paths and add the tags based on the thesaurus. At the end, a message will be displayed with the results, including:
- **Errors**: The files that encountered an error and the specific error message.
- **Skipped files**: The files that have been skipped because no synonym was found.
- **Success**: The files that have been successfully parsed, along with the tags that have been added.
2. <ins>My Thesaurus: Parse current file</ins>: This command will parse the current file and add the tags based on the thesaurus.
## ⚙️ Settings
1. <ins>Thesaurus</ins>: Path to the file containing the thesaurus. The file must be a CSV file and must be in your vault. You can click on the `save` button to ensure the file is valid (the path is correct, the file exists, and it's well-formed).
> [!CAUTION]
> If, for some reason, the file is moved, you need to update the path in the settings! It won't be updated automatically.
2. <ins>Separator</ins>: The separator used in the CSV file. Supported separators are `,`, `;`, `|`, `tabulation` and `markdown`. See the note above for the markdown separator.
3. <ins>Column title</ins>: The title of the column in the CSV file that contains the tags:
- **Term**: Name of the tag column **to be added**
- **Synonyms**: Name of the column of **words to be searched in the file** (contents and file title).
4. <ins>Included paths</ins>: Files in these folders will be included when using the command `My Thesaurus: Parse all files`. You can separate the paths with a comma, semicolon, or newline. Moreover, regex is supported.
> [!TIP]
> You can include all the files in your vault by using `.*` as the path!
5. <ins>Excluded terms</ins>: Allow to exclude term using a frontmatter key. Include two settings:
- **Frontmatter key**: The key to search in the frontmatter, by default `exclude_term`
- **Auto-clean**: If enabled, the plugin will clean the already existing tags with removing the excluded terms.
6. <ins>Remove accents</ins>: If enabled, accents will be suppressed when searching for synonyms. Allows `café` to be recognized by `cafe` (and vice versa).
## 📝 Usage
The plugin has two commands:
1. <ins>My Thesaurus: Parse all files</ins>: This command will parse all the files in the included paths and add the tags based on the thesaurus. At the end, a message will be displayed with the results, including:
- **Errors**: The files that encountered an error and the specific error message.
- **Skipped files**: The files that have been skipped because no synonym was found.
- **Success**: The files that have been successfully parsed, along with the tags that have been added.
2. <ins>My Thesaurus: Parse current file</ins>: This command will parse the current file and add the tags based on the thesaurus.
## 📥 Installation
- [ ] From Obsidian's community plugins
- [x] Using BRAT with `https://github.com/Mara-Li/`
- [x] From the release page:
- [x] From the release page:
- Download the latest release
- Unzip `my-thesaurus.zip` in `.obsidian/plugins/` path
- In Obsidian settings, reload the plugin
- Enable the plugin
### 🎼 Languages
- [x] English
- [x] French
To add a translation:
1. Fork the repository
2. Add the translation in the `src/i18n/locales` folder with the name of the language (ex: `fr.json`).
2. Add the translation in the `src/i18n/locales` folder with the name of the language (ex: `fr.json`).
- You can get your locale language from Obsidian using [obsidian translation](https://github.com/obsidianmd/obsidian-translations) or using the commands (in templater for example) : `<% tp.obsidian.moment.locale() %>`
- Copy the content of the [`en.json`](./src/i18n/locales/en.json) file in the new file
- Translate the content
@ -56,3 +102,7 @@ To add a translation:
- Add `import * as <lang> from "./locales/<lang>.json";`
- Edit the `ressource` part with adding : `<lang> : {translation: <lang>}`
---
# Credits
- Inspired by [pmartinolli/MyThesaurus](https://github.com/pmartinolli/MyThesaurus)

View file

@ -1,9 +1,9 @@
{
"id": "my-thesaurus",
"name": "My Thesaurus",
"version": "0.0.2",
"version": "0.0.10",
"minAppVersion": "1.5.12",
"description": "Automagically adds tags based on a simple csv file.",
"description": "Automagically adds tags based on a simple csv file or a Markdown table.",
"author": "mara-li",
"authorUrl": "https//github.com/mara-li",
"isDesktopOnly": false

View file

@ -1,10 +1,10 @@
{
"id": "my-thesaurus",
"name": "My Thesaurus",
"version": "0.0.2",
"version": "0.0.10",
"minAppVersion": "1.5.12",
"description": "Automagically adds tags based on a simple csv file.",
"description": "Automagically adds tags based on a simple csv file or a Markdown table.",
"author": "mara-li",
"authorUrl": "https//github.com/mara-li",
"authorUrl": "https://github.com/mara-li",
"isDesktopOnly": false
}

View file

@ -1,7 +1,7 @@
{
"name": "my-thesaurus",
"version": "0.0.2",
"description": "Add automagically tags based on a thesaurus CSV.",
"version": "0.0.10",
"description": "Add automagically tags based on a thesaurus (CSV or markdown table).",
"main": "main.js",
"private": true,
"scripts": {
@ -10,8 +10,8 @@
"dev:prod": "node esbuild.config.mjs --vault",
"dev": "node esbuild.config.mjs",
"export": "node esbuild.config.mjs --production --vault",
"bump": "dotenv -- node commit-and-tag-version.mjs",
"postbump": "dotenv -- if-env SKIP_POST_BUMP=true && echo skip bump || git push --follow-tags origin master",
"bump": "node commit-and-tag-version.mjs",
"postbump": "git push --follow-tags origin master",
"predeploy": "bun run bump",
"deploy": "pnpm run export",
"lint": "pnpm biome format --write src/"

View file

@ -1,67 +1,87 @@
{
"command": {
"active": "Add thesaurus tags to active file",
"all": "Parse all"
},
"error": {
"csv": {
"columns": "The columns doesn't correspond with the settings.",
"header": "You can have only two column in your thesaurus. You have: {{- len}} columns.",
"malformed": "Your CSV is malformed: {{- len}} columns found.",
"separator": "The specified separator is invalid. You seems to use \"{{- sep}}\" as separator."
},
"file": {
"empty": "The file {{- file}} is empty!"
},
"noFiles": "No files to parse found!",
"noPaths": "No file paths have been included in the parameters.",
"notFound": "The thesaurus is not found at the provided path ({{- path}})."
},
"modal": {
"errors": "Errors",
"skips": "Skipped",
"success": "Success",
"title": "MyThesaurus - Results"
},
"notice": {
"done": "Done !",
"loading": "Loading...",
"start": "Start treatment..."
},
"settings": {
"columns": {
"synonyms": {
"desc": "These values will be recognized in the file content and the corresponding term will be applied.",
"title": "Synonym"
},
"term": {
"desc": "This value will be set as a tag in your properties.",
"title": "Term"
},
"title": "Column title"
},
"includedPaths": {
"desc": "Includes files from its paths when using the global command.",
"regex": "Regex are supported.",
"separate": "Separate paths with coma, semicolon, spaces or newlines.",
"title": "Included paths"
},
"separator": {
"desc": "CSV column separator.",
"tab": "Tabulation",
"title": "Separator"
},
"thesaurusPath": {
"desc": "Path to your CSV thesaurus (in your vault).",
"error": "The provided file is not valide.",
"placeholder": "mon_thesaurus.csv",
"success": "The provided file is correct!",
"title": "Thesaurus",
"verify": "Verify validity of the file"
}
},
"success": {
"none": "No tag to add!",
"title": "The tags has been added:"
}
}
"command": {
"active": "Add thesaurus tags to active file",
"all": "Parse all"
},
"error": {
"csv": {
"columns": "The columns doesn't correspond with the settings.",
"header": "You can have only two column in your thesaurus. You have: {{- len}} columns.",
"malformed": "Your CSV is malformed: there is no column.",
"separator": "The specified separator is invalid. You seems to use \"{{- sep}}\" as separator."
},
"file": {
"empty": "The file {{- file}} is empty!"
},
"noFiles": "No files to parse found!",
"noPath": "The path to the thesaurus is empty!",
"noPaths": "No file paths have been included in the parameters.",
"notFound": "The thesaurus is not found at the provided path ({{- path}})."
},
"modal": {
"errors": "Errors",
"skips": "Skipped",
"success": "Success",
"title": "MyThesaurus - Results"
},
"notice": {
"done": "Done !",
"loading": "Loading...",
"start": "Start treatment..."
},
"settings": {
"columns": {
"synonyms": {
"desc": "This value is the name of the column of words to be searched in the file (contents and title).",
"title": "Synonym"
},
"term": {
"desc": "This value is the name of the tag column to be added.",
"title": "Term"
},
"title": "Column title"
},
"exclude": {
"clean": {
"desc": "Automatically deletes existing tags if a term is excluded.",
"title": "Auto-clean"
},
"desc": "Allow to exclude term using a property key.",
"key": "Frontmatter key",
"title": "Excluding terms"
},
"includedPaths": {
"desc": "Includes files from its paths when using the global command.",
"regex": "Regex are supported.",
"separate": "Separate paths with coma, semicolon, spaces or newlines.",
"title": "Included paths"
},
"other": "Other",
"removeAccents": {
"by": "like",
"cafe": "cafe",
"café": "café",
"desc": "Ignores accents during recognition: helps identify",
"title": "Remove accents",
"viceversa": "(and vice versa)"
},
"separator": {
"desc": "CSV column separator.",
"md": "Markdown table",
"tab": "Tabulation",
"title": "Separator"
},
"thesaurusPath": {
"desc": "Path to your CSV thesaurus (in your vault).",
"error": "The provided file is not valide.",
"placeholder": "mon_thesaurus.csv",
"success": "The provided file is correct!",
"title": "Thesaurus",
"verify": "Verify validity of the file"
}
},
"success": {
"none": "No tag to add!",
"title": "The tags has been added:"
}
}

View file

@ -7,13 +7,14 @@
"csv": {
"columns": "Les colonnes du CSV fourni ne correspondent pas avec les colonnes des paramètres.",
"header": "Vous ne pouvez avoir que deux colonnes dans votre thésaurus. Vous avez {{- len}} colonnes.",
"malformed": "Votre CSV est mal formé : {{- len}} colonnes trouvées.",
"malformed": "Votre CSV est mal formé : aucune colonne n'a été trouvées.",
"separator": "Le séparateur spécifié n'est pas le bon. Vous semblez utiliser \"{{- sep}}\" comme séparateur."
},
"file": {
"empty": "Le fichier {{- file}} est vide !"
},
"noFiles": "Aucun fichier n'a été trouvé !",
"noPath": "Le chemin vers le thésaurus est vide !",
"noPaths": "Aucun chemins de fichiers n'a été inclus dans les paramètres.",
"notFound": "Le thésaurus est introuvable au chemin fourni ({{-path}})."
},
@ -31,23 +32,42 @@
"settings": {
"columns": {
"synonyms": {
"desc": "Ces valeurs seront reconnus dans le contenu du fichier et le terme correspondant sera appliqué.",
"desc": "Cette valeur est le nom de la colonne des mots à chercher dans le fichier (contenu et titre).",
"title": "Synonymes"
},
"term": {
"desc": "Cette valeur sera ajouté comme tag dans vos propriétés.",
"desc": "Cette valeur est le nom de la colonne des tags à ajouter.",
"title": "Terme"
},
"title": "Colonne cible"
},
"exclude": {
"clean": {
"desc": "Supprime automatiquement les tags déjà existant si un terme est exclu.",
"title": "Auto-nettoyage"
},
"desc": "Permet d'exclure des termes en utilisant une clé de propriétés",
"key": "Clé de propriétés",
"title": "Exclure des termes"
},
"includedPaths": {
"desc": "Inclus les fichiers depuis ses chemins lors d'utilisation de la commande globale.",
"regex": "Les expressions régulières sont supportées.",
"separate": "Séparer les chemins par une virgule, point virgule ou un retour à la ligne.",
"title": "Chemins inclus"
},
"other": "Autres",
"removeAccents": {
"by": "comme",
"cafe": "cafe",
"café": "café",
"desc": "Ignore les accents lors de la reconnaissance : permet d'identifier",
"title": "Supprimer les accents",
"viceversa": "(et inversement)"
},
"separator": {
"desc": "Séparateur des colonnes du fichier CSV.",
"md": "Markdown table",
"tab": "Tabulation",
"title": "Séparateur"
},

View file

@ -5,6 +5,9 @@ export interface MyThesaurusSettings {
includedPaths: string[];
separator: Separator;
columns: ColumnName;
removeAccents: boolean;
excludeTermKey: string;
cleanExcludedTerms: boolean;
}
export type ColumnName = {
@ -21,17 +24,20 @@ export type ParseResult = {
export type ParseResults = ParseResult[];
export type Separator = ";" | "," | "\t" | "|";
export type Separator = ";" | "," | "\t" | "|" | "md";
export type Thesaurus = Record<string, Set<string>>;
export type Translation = TFunction<"translation", undefined>;
export const DEFAULT_SETTINGS: MyThesaurusSettings = {
thesaurusPath: "",
includedPaths: [],
includedPaths: [".*"],
separator: ";",
columns: {
term: "term",
synonyms: "synonyms",
},
removeAccents: false,
excludeTermKey: "exclude_term",
cleanExcludedTerms: false,
};

View file

@ -1,5 +1,12 @@
import i18next from "i18next";
import { Notice, Plugin, TFile, sanitizeHTMLToDom } from "obsidian";
import {
type FrontMatterCache,
Notice,
Plugin,
TFile,
getFrontMatterInfo,
sanitizeHTMLToDom,
} from "obsidian";
import { resources, translationLanguage } from "./i18n";
import {
DEFAULT_SETTINGS,
@ -11,24 +18,64 @@ import {
import { MyThesaurusSettingTab } from "./settings";
import "uniformize";
import { ResultModals } from "./modals";
import { getTags, getThesaurus } from "./utils";
import {
areArraysEqual,
findMissingElements,
getKeysAsArray,
getTags,
getThesaurus,
} from "./utils";
export default class MyThesaurus extends Plugin {
settings!: MyThesaurusSettings;
async addTagsToNote(tags: string[], file: TFile) {
const currentTags = this.app.metadataCache.getFileCache(file)?.tags || [];
const newTags = [...new Set([...currentTags, ...tags])];
if (newTags.length === 0) return;
getFileTags(frontmatter: FrontMatterCache) {
const tags = [];
if (frontmatter.tags) tags.push(...getKeysAsArray(frontmatter.tags));
if (frontmatter.tag) tags.push(...getKeysAsArray(frontmatter.tag));
return tags;
}
filterTerm(frontmatter: FrontMatterCache, tags: Set<string>) {
const keyToFind = this.settings.excludeTermKey;
if (keyToFind.trim().length === 0) return [...tags];
if (frontmatter?.[keyToFind]) {
const excludeList = getKeysAsArray(frontmatter[keyToFind]);
for (const tag of excludeList) tags.delete(tag);
}
return [...tags];
}
async addTagsToNote(
tags: string[],
file: TFile,
currentTags: string[],
frontmatter: FrontMatterCache
): Promise<boolean> {
if (areArraysEqual(tags, currentTags)) return false;
const updatedTags = new Set([...currentTags, ...tags]);
let newTags = [...updatedTags];
if (this.settings.cleanExcludedTerms)
newTags = this.filterTerm(frontmatter, updatedTags);
if (newTags.length === 0) return false;
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
frontmatter.tags = newTags;
if (frontmatter.tag) frontmatter.tag = newTags;
else frontmatter.tags = newTags;
});
return true;
}
notice(message: string | DocumentFragment, silent = false) {
if (!silent) {
new Notice(message);
}
if (!silent) new Notice(message);
}
skip(file: TFile, silent = false): ParseResult {
this.notice(i18next.t("success.none"), silent);
return {
file: file.basename,
tags: [],
type: "skip",
};
}
async parseFile(
@ -48,26 +95,29 @@ export default class MyThesaurus extends Plugin {
};
}
try {
const tags = getTags(contents, thesaurus);
if (tags.length > 0) {
await this.addTagsToNote(tags, file);
const successMsg = sanitizeHTMLToDom(
`<span class="success">${i18next.t("success.title")}</span>${tags.map((tag) => `<li><code>${tag}</code></li>`).join("")}`
);
this.notice(successMsg, silent);
return {
file: file.basename,
tags: tags,
type: "success",
};
} else {
this.notice(i18next.t("success.none"), silent);
return {
file: file.basename,
tags: [],
type: "skip",
};
}
const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter || {};
const tags = getTags(contents, thesaurus, this.settings.removeAccents).concat(
...getTags(file.basename, thesaurus, this.settings.removeAccents)
);
const tagsToAdd = this.filterTerm(frontmatter, new Set(tags));
if (tagsToAdd.length > 0) {
const currentTags = this.getFileTags(frontmatter);
const added = await this.addTagsToNote(tagsToAdd, file, currentTags, frontmatter);
if (added) {
const missingTags = findMissingElements(currentTags, tagsToAdd);
if (missingTags.length === 0) return this.skip(file, silent);
const successMsg = sanitizeHTMLToDom(
`<span class="success">${i18next.t("success.title")}</span>${missingTags.map((tag) => `<li><code>${tag}</code></li>`).join("")}`
);
this.notice(successMsg, silent);
return {
file: file.basename,
tags: tagsToAdd,
type: "success",
};
} else return this.skip(file, silent);
} else return this.skip(file, silent);
} catch (error) {
this.notice(
sanitizeHTMLToDom(`<span class="errors">${(error as Error).message}</span>`),
@ -83,27 +133,37 @@ export default class MyThesaurus extends Plugin {
}
async readThesaurus() {
if (this.settings.thesaurusPath.length === 0)
throw new Error(i18next.t("error.noPath"));
const thesaurusFile = this.app.vault.getAbstractFileByPath(
this.settings.thesaurusPath
);
const ext = this.settings.separator === "md" ? "md" : "csv";
if (
!thesaurusFile ||
!(thesaurusFile instanceof TFile) ||
thesaurusFile.extension !== "csv"
thesaurusFile.extension !== ext
) {
throw new Error(i18next.t("error.notFound"));
}
return await this.app.vault.read(thesaurusFile);
let contents = await this.app.vault.read(thesaurusFile);
if (ext === "md") {
contents = contents.replace(/^\s*\n/gm, "");
//remove the frontmatter
const frontmatterInfo = getFrontMatterInfo(contents);
if (frontmatterInfo.exists) return contents.slice(frontmatterInfo.contentStart);
return contents;
}
return contents;
}
scanAllFiles(): TFile[] {
const allMarkdownFiles = this.app.vault.getMarkdownFiles();
console.log(allMarkdownFiles);
const files = [];
for (const filter of this.settings.includedPaths) {
try {
const regex = new RegExp(filter, "g");
console.log(regex);
files.push(...allMarkdownFiles.filter((file) => file.path.match(regex)));
} catch (error) {
console.error(error);
@ -132,7 +192,8 @@ export default class MyThesaurus extends Plugin {
await this.readThesaurus(),
this.settings.separator,
i18next.t,
this.settings.columns
this.settings.columns,
this.settings.removeAccents
);
const results: ParseResults = [];
const noticeBar = new Notice(
@ -188,7 +249,8 @@ export default class MyThesaurus extends Plugin {
thesaurusContent,
this.settings.separator,
i18next.t,
this.settings.columns
this.settings.columns,
this.settings.removeAccents
);
this.parseFile(file, false, thesaurus);
});

View file

@ -1,12 +1,15 @@
import { AbstractInputSuggest, type App, type TFile } from "obsidian";
export class FileSuggester extends AbstractInputSuggest<TFile> {
ext: "csv" | "md" = "csv";
constructor(
private inputEl: HTMLInputElement,
app: App,
ext: "csv" | "md",
private onSubmit: (value: TFile) => void
) {
super(app, inputEl);
this.ext = ext;
}
renderSuggestion(value: TFile, el: HTMLElement): void {
@ -14,7 +17,9 @@ export class FileSuggester extends AbstractInputSuggest<TFile> {
}
getSuggestions(query: string): TFile[] {
const allFiles = this.app.vault.getFiles().filter((file) => file.extension === "csv");
const allFiles = this.app.vault
.getFiles()
.filter((file) => file.extension === this.ext);
return allFiles.filter((file) => file.path.subText(query));
}

View file

@ -15,11 +15,13 @@ import { getThesaurus } from "./utils";
export class MyThesaurusSettingTab extends PluginSettingTab {
plugin: MyThesaurus;
settings: MyThesaurusSettings;
isInvalid: boolean;
constructor(app: App, plugin: MyThesaurus) {
super(app, plugin);
this.plugin = plugin;
this.settings = plugin.settings;
this.isInvalid = false;
}
display(): void {
@ -28,19 +30,82 @@ export class MyThesaurusSettingTab extends PluginSettingTab {
containerEl.empty();
const thesaurusSetting = new Setting(containerEl)
.setHeading()
.setName(i18next.t("settings.thesaurusPath.title"))
.setDesc(i18next.t("settings.thesaurusPath.desc"))
.addSearch(async (cb) => {
cb.setPlaceholder(i18next.t("settings.thesaurusPath.placeholder"));
cb.setValue(this.settings.thesaurusPath);
const ext = this.settings.separator === "md" ? "md" : "csv";
new FileSuggester(cb.inputEl, this.app, ext, async (result) => {
this.settings.thesaurusPath = result.path;
await this.plugin.saveSettings();
});
//clean when the cb.clear() is called
cb.clearButtonEl.addEventListener("click", async () => {
this.settings.thesaurusPath = "";
await this.plugin.saveSettings();
});
})
.addExtraButton((button) => {
button
.setIcon("save")
.setTooltip(i18next.t("settings.thesaurusPath.verify"))
.onClick(async () => {
const file = this.app.vault.getAbstractFileByPath(
this.settings.thesaurusPath
);
if (!(file instanceof TFile)) {
new Notice(i18next.t("settings.thesaurusPath.error"));
this.isInvalid = true;
this.display();
return;
}
const content = await this.plugin.readThesaurus();
try {
getThesaurus(
content,
this.settings.separator,
i18next.t,
this.settings.columns,
this.settings.removeAccents
);
this.isInvalid = false;
this.display();
} catch (e) {
new Notice(
sanitizeHTMLToDom(`<span class="error">${(e as Error).message}</span>`)
);
this.isInvalid = true;
this.display();
return;
}
new Notice(
sanitizeHTMLToDom(
`<span class="success">${i18next.t("settings.thesaurusPath.success")}</span>`
)
);
});
});
if (this.isInvalid) thesaurusSetting.setClass("is-invalid");
new Setting(containerEl)
.setName(i18next.t("settings.separator.title"))
.setDesc(i18next.t("settings.separator.desc"))
.setClass("no-border")
.addDropdown((dropdown) => {
dropdown
.addOption(";", ";")
.addOption(",", ",")
.addOption("\t", `${i18next.t("settings.separator.tab")}`)
.addOption("|", "|")
.addOption("md", `${i18next.t("settings.separator.md")}`)
.setValue(this.settings.separator)
.onChange(async (value) => {
this.settings.separator = value as Separator;
await this.plugin.saveSettings();
this.display();
});
});
@ -48,6 +113,7 @@ export class MyThesaurusSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName(i18next.t("settings.columns.term.title"))
.setClass("no-border")
.setDesc(i18next.t("settings.columns.term.desc"))
.addText((text) => {
text.setValue(this.settings.columns.term).onChange(async (value) => {
@ -57,6 +123,7 @@ export class MyThesaurusSettingTab extends PluginSettingTab {
});
new Setting(containerEl)
.setClass("no-border")
.setName(i18next.t("settings.columns.synonyms.title"))
.setDesc(i18next.t("settings.columns.synonyms.desc"))
.addText((text) => {
@ -64,65 +131,67 @@ export class MyThesaurusSettingTab extends PluginSettingTab {
this.settings.columns.synonyms = value;
await this.plugin.saveSettings();
});
new Setting(containerEl)
.setHeading()
.setName(i18next.t("settings.thesaurusPath.title"))
.setDesc(i18next.t("settings.thesaurusPath.desc"))
.addSearch(async (cb) => {
cb.setPlaceholder(i18next.t("settings.thesaurusPath.placeholder"));
cb.setValue(this.settings.thesaurusPath);
new FileSuggester(cb.inputEl, this.app, async (result) => {
this.settings.thesaurusPath = result.path;
await this.plugin.saveSettings();
});
})
.addExtraButton((button) => {
button
.setIcon("save")
.setTooltip(i18next.t("settings.thesaurusPath.verify"))
.onClick(async () => {
const file = this.app.vault.getAbstractFileByPath(
this.settings.thesaurusPath
);
if (!(file instanceof TFile)) {
new Notice(i18next.t("settings.thesaurusPath.error"));
return;
}
const content = await this.app.vault.read(file);
try {
getThesaurus(
content,
this.settings.separator,
i18next.t,
this.settings.columns
);
} catch (e) {
new Notice((e as Error).message);
this.settings.thesaurusPath = "";
await this.plugin.saveSettings();
this.display();
return;
}
new Notice(i18next.t("settings.thesaurusPath.success"));
});
});
});
new Setting(containerEl)
.setName(i18next.t("settings.includedPaths.title"))
.setHeading()
.setClass("border-top")
.setDesc(
sanitizeHTMLToDom(
`${i18next.t("settings.includedPaths.desc")}<br>${i18next.t("settings.includedPaths.separate")}<br>${i18next.t("settings.includedPaths.regex")}`
)
)
.addTextArea((text) => {
text.setValue(this.settings.includedPaths.join("\n")).onChange(async (value) => {
this.settings.includedPaths = value
.split(/[\n,;]+/)
.map((path) => path.trim())
.filter((path) => path.length > 0);
text
.setPlaceholder(".*")
.setValue(this.settings.includedPaths.join("\n"))
.onChange(async (value) => {
this.settings.includedPaths = value
.split(/[\n,;]+/)
.map((path) => path.trim())
.filter((path) => path.length > 0);
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName(i18next.t("settings.exclude.title"))
.setHeading()
.setDesc(i18next.t("settings.exclude.desc"));
new Setting(containerEl)
.setName(i18next.t("settings.exclude.key"))
.setClass("no-border")
.addText((text) => {
text.setValue(this.settings.excludeTermKey).onChange(async (value) => {
this.settings.excludeTermKey = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName(i18next.t("settings.exclude.clean.title"))
.setDesc(i18next.t("settings.exclude.clean.desc"))
.setClass("no-border")
.addToggle((toggle) => {
toggle.setValue(this.settings.cleanExcludedTerms).onChange(async (value) => {
this.settings.cleanExcludedTerms = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl).setHeading().setName(i18next.t("settings.other"));
new Setting(containerEl)
.setName(i18next.t("settings.removeAccents.title"))
.setClass("no-border")
.setDesc(
sanitizeHTMLToDom(
`${i18next.t("settings.removeAccents.desc")}<code>"${i18next.t("settings.removeAccents.café")}"</code>${i18next.t("settings.removeAccents.by")}<code>"${i18next.t("settings.removeAccents.cafe")}"</code>${i18next.t("settings.removeAccents.viceversa")}.`
)
)
.addToggle((toggle) => {
toggle.setValue(this.settings.removeAccents).onChange(async (value) => {
this.settings.removeAccents = value;
await this.plugin.saveSettings();
});
});

View file

@ -57,3 +57,21 @@
.my-thesaurus.modal-title {
text-align: center;
}
.my-thesaurus .is-invalid input {
border-color: var(--text-error);
box-shadow: none;
}
.my-thesaurus .no-border {
border: none;
}
.my-thesaurus .setting-item-heading {
--font-ui-medium: 1.25rem;
border-bottom: 1px solid var(--background-modifier-border);
}
.my-thesaurus .border-top {
border-top: 1px solid var(--background-modifier-border);
}

View file

@ -1,4 +1,20 @@
import { getThesaurus } from "./parse_csv";
import { getHeader, getThesaurus } from "./parse_csv";
import { getTags } from "./parse_file";
export { getThesaurus, getTags };
export { getThesaurus, getTags, getHeader };
export function areArraysEqual<T>(arr1?: T[], arr2?: T[]): boolean {
if ((arr1 && !arr2) || (!arr1 && arr2) || (!arr1 && !arr2)) return false;
if (arr1 && arr2)
return arr1.length === arr2.length && arr1.every((val, i) => val === arr2[i]);
return false;
}
export function findMissingElements<T>(currentTags: T[], tags: T[]): T[] {
const currentSet = new Set(currentTags);
return tags.filter((tag) => !currentSet.has(tag));
}
export function getKeysAsArray(tag: string | string[]) {
if (!Array.isArray(tag)) return [tag];
return tag;
}

View file

@ -1,70 +1,71 @@
import type { ColumnName, Separator, Thesaurus, Translation } from "../interfaces";
const separatorRegex = /[,;\t\|]/;
function verifySeparator(header: string, sep: Separator): boolean {
return header.includes(sep);
}
function searchSeparator(header: string): Separator {
return header.match(/[,;\t\|]/)?.[0] as Separator;
return header.match(separatorRegex)?.[0] as Separator;
}
function getColumn(
function countColumn(header: string) {
return header.split(separatorRegex).length;
}
export function getColumn(
header: string[],
columnNames: ColumnName,
ln: Translation
): { indexKey: number; indexSynonyms: number } {
const col1 = header[0].trim().standardize();
const col2 = header[1].trim().standardize();
let indexKey = 1;
let indexSynonyms = 0;
const standardized = structuredClone(columnNames);
standardized.term = standardized.term.standardize();
standardized.synonyms = standardized.synonyms.standardize();
if (
(col1 !== standardized.term && col1 !== standardized.synonyms) ||
(col2 !== standardized.term && col2 !== standardized.synonyms)
) {
throw new Error(ln("error.csv.columns"));
}
if (col1 === standardized.term) {
indexKey = 0;
indexSynonyms = 1;
}
const indexSynonyms = header.findIndex(
(x) => x.standardize() === standardized.synonyms
);
const indexKey = header.findIndex((x) => x.standardize() === standardized.term);
if (indexKey === -1 || indexSynonyms === -1) throw new Error(ln("error.csv.columns"));
return { indexKey, indexSynonyms };
}
export function getHeader(fileContent: string[], separator: Separator) {
return fileContent[0]
.split(separator)
.filter((col) => col.trim() !== "")
.map((col) => col.trim());
}
export function getThesaurus(
fileContent: string,
separator: Separator,
ln: Translation,
columnNames: ColumnName
columnNames: ColumnName,
standardize?: boolean
) {
//verify if they are only two columns
if (fileContent.trim().length === 0) throw new Error(ln("error.csv.malformed"));
const isMd = separator === "md";
separator = isMd ? "|" : separator;
const lines = fileContent.split("\n");
if (countColumn(lines[0]) <= 1) throw new Error(ln("error.csv.malformed"));
if (!verifySeparator(lines[0], separator))
throw new Error(ln("error.csv.separator", { sep: searchSeparator(lines[0]) }));
const header = lines[0].split(separator);
if (header.length !== 2) {
throw new Error(ln("error.csv.header", { len: header.length }));
}
const header = getHeader(lines, separator);
if (header.length <= 1) throw new Error(ln("error.csv.malformed"));
const { indexKey, indexSynonyms } = getColumn(header, columnNames, ln);
const thesaurus: Thesaurus = {};
for (const line of lines.slice(1).filter((line) => line.trim() !== "")) {
const columns = line.split(separator);
if (columns.length > 0 && columns.length !== 2)
throw new Error(ln("error.csv.malformed", { len: columns.length }));
const lineSlice = isMd ? 2 : 1;
for (const line of lines.slice(lineSlice).filter((line) => line.trim() !== "")) {
const columns = line.split(separator).filter((col) => col.trim() !== "");
const key = columns[indexKey].trim();
const synonyms = columns[indexSynonyms].trim();
let synonyms = columns[indexSynonyms].trim();
if (standardize) synonyms = synonyms.standardize();
if (key !== "" && synonyms !== "") {
if (thesaurus[key] === undefined) {
thesaurus[key] = new Set();
thesaurus[key].add(synonyms);
} else {
thesaurus[key].add(synonyms);
}
if (thesaurus[key] === undefined) thesaurus[key] = new Set();
thesaurus[key].add(synonyms);
}
}
return thesaurus;

View file

@ -1,8 +1,12 @@
import type { Thesaurus } from "../interfaces";
export function getTags(content: string, thesaurus: Thesaurus) {
export function getTags(
content: string,
thesaurus: Thesaurus,
standardize?: boolean
): string[] {
const tagsToAdd: string[] = [];
if (standardize) content = content.standardize();
for (const tags of Object.keys(thesaurus)) {
const synonyms = thesaurus[tags];
const regex = new RegExp(`\\b(${[...synonyms].join("|")})\\b`, "gi");

13
tests/__constants__.ts Normal file
View file

@ -0,0 +1,13 @@
import type { ColumnName, Translation } from "../src/interfaces";
export const mockTranslation = ((key: string, options?: any) => {
const translations: { [key: string]: string } = {
"error.csv.columns": "Invalid column names",
"error.csv.separator": `Invalid separator: ${options?.sep}`,
"error.csv.header": `Invalid header length: ${options?.len}`,
"error.csv.malformed": `Your CSV is malformed: there is no column.`,
};
return translations[key] || key;
}) as Translation;
export const columnNames: ColumnName = { term: "Term", synonyms: "Synonyms" };

View file

@ -1,20 +1,12 @@
import { describe, expect, it } from "bun:test";
import type { ColumnName, Separator, Translation } from "../src/interfaces";
import { getThesaurus } from "../src/utils";
import type { Separator } from "../src/interfaces";
import { getHeader, getThesaurus } from "../src/utils";
import "uniformize";
import { getColumn } from "../src/utils/parse_csv";
import { columnNames, mockTranslation } from "./__constants__";
const mockTranslation = ((key: string, options?: any) => {
const translations: { [key: string]: string } = {
"error.csv.columns": "Invalid column names",
"error.csv.separator": `Invalid separator: ${options?.sep}`,
"error.csv.header": `Invalid header length: ${options?.len}`,
"error.csv.malformed": `Malformed line with length: ${options?.len}`,
};
return translations[key] || key;
}) as Translation;
const columnNames: ColumnName = { term: "Term", synonyms: "Synonyms" };
describe("getThesaurus", () => {
it("should parse a valid CSV content", () => {
it("passing", () => {
const csvContent = "Term,Synonyms\nword1,synonym1\nword2,synonym2";
const separator: Separator = ",";
const thesaurus = getThesaurus(csvContent, separator, mockTranslation, columnNames);
@ -25,7 +17,7 @@ describe("getThesaurus", () => {
});
});
describe("error handling", () => {
it("should throw an error for invalid column names", () => {
it("invalid column names", () => {
const csvContent = "Invalid,Header\nword1,synonym1";
const separator: Separator = ",";
expect(() =>
@ -33,7 +25,7 @@ describe("error handling", () => {
).toThrow("Invalid column names");
});
it("should throw an error for invalid separator", () => {
it("invalid separator", () => {
const csvContent = "Term;Synonyms\nword1;synonym1";
const separator: Separator = ",";
expect(() =>
@ -41,24 +33,16 @@ describe("error handling", () => {
).toThrow("Invalid separator: ;");
});
it("should throw an error for invalid header length", () => {
const csvContent = "Term,Synonyms,Extra\nword1,synonym1";
it("malformed line", () => {
const csvContent = "xxx,xxxx\nword1,synonym1";
const separator: Separator = ",";
expect(() =>
getThesaurus(csvContent, separator, mockTranslation, columnNames)
).toThrow("Invalid header length: 3");
});
it("should throw an error for malformed line", () => {
const csvContent = "Term,Synonyms\nword1,synonym1\nword2";
const separator: Separator = ",";
expect(() =>
getThesaurus(csvContent, separator, mockTranslation, columnNames)
).toThrow("Malformed line with length: 1");
).toThrow(mockTranslation("error.csv.columns"));
});
});
describe("duplicate terms and synonyms", () => {
it("should not have duplicate terms in the result", () => {
it("terms", () => {
const csvContent = "Term,Synonyms\nword1,synonym1\nword1,synonym2";
const separator: Separator = ",";
const thesaurus = getThesaurus(csvContent, separator, mockTranslation, columnNames);
@ -66,7 +50,7 @@ describe("duplicate terms and synonyms", () => {
expect(thesaurus["word1"]).toEqual(new Set(["synonym1", "synonym2"]));
});
it("should not have duplicate synonyms in the result", () => {
it("synonyms", () => {
const csvContent = "Term,Synonyms\nword1,synonym1\nword1,synonym1";
const separator: Separator = ",";
const thesaurus = getThesaurus(csvContent, separator, mockTranslation, columnNames);
@ -74,7 +58,7 @@ describe("duplicate terms and synonyms", () => {
expect(thesaurus["word1"]).toEqual(new Set(["synonym1"]));
});
it("should not have duplicate terms and synonyms in the result", () => {
it("terms and synonyms", () => {
const csvContent = "Term,Synonyms\nword1,synonym1\nword2,synonym1";
const separator: Separator = ",";
const thesaurus = getThesaurus(csvContent, separator, mockTranslation, columnNames);
@ -83,3 +67,83 @@ describe("duplicate terms and synonyms", () => {
expect(thesaurus["word2"]).toEqual(new Set(["synonym1"]));
});
});
it("with accents", () => {
const csvContent = "Term,Synonyms\ncafé,café\nrôle,rôle";
const expected = {
café: new Set(["cafe"]),
rôle: new Set(["role"]),
};
const separator: Separator = ",";
const thesaurus = getThesaurus(
csvContent,
separator,
mockTranslation,
columnNames,
true
);
expect(thesaurus).toEqual(expected);
});
describe("more than two column", () => {
const csvContent = "Term,Synonyms,Extra\nword1,synonym1, notes";
const separator: Separator = ",";
it("pass with a simple csv", () => {
const expectedColumnIndex = {
indexKey: 0,
indexSynonyms: 1,
};
const header = getHeader(csvContent.split("\n"), separator);
const column = getColumn(header, columnNames, mockTranslation);
expect(column).toEqual(expectedColumnIndex);
});
it("should error for invalid column names", () => {
const columnNames = { term: "Terms", synonyms: "Synonyme" };
const header = getHeader(csvContent.split("\n"), separator);
expect(() => getColumn(header, columnNames, mockTranslation)).toThrow(
"Invalid column names"
);
});
it("thesaurus pass", () => {
const thesaurus = getThesaurus(csvContent, separator, mockTranslation, columnNames);
expect(thesaurus).toEqual({
word1: new Set(["synonym1"]),
});
});
});
describe("Unordered columns", () => {
const csvContent = "Term, Definition, Synonyms\nword1, definition1, synonym1";
const separator: Separator = ",";
describe("should pass", () => {
const expectedColumnIndex = {
indexKey: 0,
indexSynonyms: 2,
};
const header = getHeader(csvContent.split("\n"), separator);
const column = getColumn(header, columnNames, mockTranslation);
expect(column).toEqual(expectedColumnIndex);
});
it("shouldn't pass : not recognize column", () => {
const invalidColumnNames = { term: "Word", synonyms: "Synonyms" };
expect(() =>
getThesaurus(csvContent, separator, mockTranslation, invalidColumnNames)
).toThrow("Invalid column names");
});
it("getThesaurus", () => {
const thesaurus = getThesaurus(csvContent, separator, mockTranslation, columnNames);
expect(thesaurus).toEqual({
word1: new Set(["synonym1"]),
});
});
});
describe("not enough columns", () => {
const csvContent = "Term\nword1,word2";
const separator: Separator = ",";
it("should error", () => {
expect(() =>
getThesaurus(csvContent, separator, mockTranslation, columnNames)
).toThrow(mockTranslation("error.csv.malformed"));
});
});

View file

@ -3,7 +3,7 @@ import type { Thesaurus } from "../src/interfaces";
import { getTags } from "../src/utils";
describe("should return tags", () => {
it("should return tags if synonyms are found in the content", () => {
it("passing test", () => {
const content = "This content contains synonym1 and synonym4.";
const thesaurus: Thesaurus = {
tag1: new Set(["synonym1", "synonym2"]),
@ -14,7 +14,7 @@ describe("should return tags", () => {
expect(result).toEqual(["tag1", "tag2"]);
});
it("should handle case insensitive matches", () => {
it("case insensitive", () => {
const content = "This content contains Synonym1 and SYNONYM4.";
const thesaurus: Thesaurus = {
tag1: new Set(["synonym1", "synonym2"]),
@ -24,7 +24,7 @@ describe("should return tags", () => {
const result = getTags(content, thesaurus);
expect(result).toEqual(["tag1", "tag2"]);
});
it("should return the two tags related to the synonyms", () => {
it("synonym on the same tags", () => {
const content = "This content contains synonym4.";
const thesaurus: Thesaurus = {
tag1: new Set(["synonym1", "synonym4"]),
@ -35,13 +35,12 @@ describe("should return tags", () => {
expect(result).toEqual(["tag1", "tag2"]);
});
it("should return the tags if the synonyms is equal to the tag", () => {
it("synonym = tag", () => {
const content = "This content contains tag1.";
const thesaurus: Thesaurus = {
tag1: new Set(["tag1", "synonym1"]),
tag2: new Set(["synonym3", "synonym4"]),
};
const result = getTags(content, thesaurus);
expect(result).toEqual(["tag1"]);
});
@ -78,3 +77,20 @@ describe("should not return tags", () => {
expect(result).toEqual([]);
});
});
describe("Find without accents", () => {
const thesaurus: Thesaurus = {
cafe: new Set(["cafe", "chocolate"]),
role: new Set(["role", "roliste"]),
};
it("should find tag with accents", () => {
const content = "This content contains café.";
const result = getTags(content, thesaurus, true);
expect(result).toEqual(["cafe"]);
});
it("should find tag without accents", () => {
const content = "This content contains cafe.";
const result = getTags(content, thesaurus, true);
expect(result).toEqual(["cafe"]);
});
});

View file

@ -0,0 +1,94 @@
import { describe, expect, it } from "bun:test";
import type { Separator } from "../src/interfaces";
import { getHeader, getThesaurus } from "../src/utils";
import { columnNames, mockTranslation } from "./__constants__";
import "uniformize";
import { getColumn } from "../src/utils/parse_csv";
it("validate header", () => {
const csvContent =
"| Term | Synonyms |\n| --- | --- |\n| word1 | synonym1\nword2 | synonym2 |";
const separator: Separator = "|";
const fileContent = csvContent.split("\n");
const header = getHeader(fileContent, separator);
expect(header).toEqual(["Term", "Synonyms"]);
});
it("passing", () => {
const csvContent =
"| Term | Synonyms |\n" +
"| ----- | -------- |\n" +
"| word1 | synonym1 |\n" +
"| word2 | synonym2 |\n";
const separator: Separator = "md";
const thesaurus = getThesaurus(csvContent, separator, mockTranslation, columnNames);
expect(thesaurus).toEqual({
word1: new Set(["synonym1"]),
word2: new Set(["synonym2"]),
});
});
it("with accents", () => {
const csvContent =
"| Term | Synonyms |\n" +
"| ----- | -------- |\n" +
"| café | café |\n" +
"| rôle | rôle |\n";
const expected = {
café: new Set(["cafe"]),
rôle: new Set(["role"]),
};
const separator: Separator = "md";
const thesaurus = getThesaurus(
csvContent,
separator,
mockTranslation,
columnNames,
true
);
expect(thesaurus).toEqual(expected);
});
describe("More than two columns", () => {
const csvContent =
"| Term | Synonyms | Definition |\n" +
"| ----- | -------- | ---------- |\n" +
"| word1 | synonym1 | definition1 |\n" +
"| word2 | synonym2 | definition2 |\n";
const expected = {
word1: new Set(["synonym1"]),
word2: new Set(["synonym2"]),
};
it("get thesaurus", () => {
const separator: Separator = "md";
const thesaurus = getThesaurus(csvContent, separator, mockTranslation, columnNames);
expect(thesaurus).toEqual(expected);
});
it("verify header", () => {
const separator: Separator = "|";
const fileContent = csvContent.split("\n");
const header = getHeader(fileContent, separator);
const expectedColumnIndex = {
indexKey: 0,
indexSynonyms: 1,
};
const column = getColumn(header, columnNames, mockTranslation);
expect(column).toEqual(expectedColumnIndex);
});
it("shouldn't pass : not recognize column", () => {
const separator: Separator = "md";
const invalidColumnNames = { term: "Word", synonyms: "Synonyms" };
expect(() =>
getThesaurus(csvContent, separator, mockTranslation, invalidColumnNames)
).toThrow(mockTranslation("error.csv.columns"));
});
it("Not ordered columns", () => {
const csvContent =
"| Term | Definition | Synonyms |\n" +
"| ----- | -------- | ---------- |\n" +
"| word1 | Def2 | synonym1 |\n" +
"| word2 | Def1 | synonym2 |\n";
const separator: Separator = "md";
const thesaurus = getThesaurus(csvContent, separator, mockTranslation, columnNames);
expect(thesaurus).toEqual({
word1: new Set(["synonym1"]),
word2: new Set(["synonym2"]),
});
});
});