fix(ai): espaces dans les noms de fichiers et chemins des liens (BUG-042)
Les noms/paths de l'assistant peuvent contenir des espaces ; la detection et la resolution les prennent desormais en charge de bout en bout : - liens markdown : cible avec espaces, encadree par <...> ou avec un "titre", et URL percent-encodees (%20) decodees ; - code inline : `Ma note.md` / `Mon dossier/Ma note.md` reconnus (_looksLikePath accepte les espaces, jeu de caracteres strict pour rejeter extraits de code et commandes shell) ; - mentions brutes : liees uniquement si presentes dans l'index du vault (_linkifySpacePaths + _confirmPathInCache), avec acceptation du plus long suffixe aligne sur un mot -> le mot de prose precedent n'est pas avale ; - _normalizeLinkPath : trim ; _renderMdLink : decodage %20. Tests : frontend IA 57/57 (+5), 9 suites JSDOM, validate-imports 36 modules, pytest 963 passed / 6 skipped.
This commit is contained in:
@@ -34,6 +34,9 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
l'arborescence, un chemin de fichier l'ouvre. Le chemin est **résolu contre
|
||||
l'index du vault** (exact → suffixe → basename unique) avant d'agir ; les
|
||||
chemins non résolus copient le nom au lieu d'afficher `File not found`.
|
||||
Les **noms/chemins contenant des espaces** sont pris en charge (`Mon dossier/Ma
|
||||
note.md`) : liens markdown (y compris cibles `<…>` et `%20`), code inline, et
|
||||
mentions brutes liées uniquement si le chemin existe dans l'index du vault.
|
||||
|
||||
### Sécurité
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
||||
| *BUG-039* | [🔵 MINEUR] Enumération de comptes : 429 (verrouillé) vs 401 (inconnu) | 🔴 ouvert | P3 | 🔐 sécurité | IA | `backend/auth/router.py:120` | Tenter un login sur un compte verrouillé puis un nom inconnu | Répondre 401 uniforme avec un timing équivalent | Le statut HTTP distingue l'existence d'un compte |
|
||||
| *BUG-040* | [🔵 MINEUR] Extraction PDF intégrale (100 ko) au scan de démarrage | 🔴 ouvert | P2 | ⚙️ backend | IA | `backend/indexer.py:465` | Démarrer sur un vault contenant de nombreux PDF | Analyser les PDF en tâche de fond / à la demande (lazy) | Ralentit fortement le démarrage et le rebuild d'index |
|
||||
| *BUG-041* | [🟡 IMPORTANT] Assistant IA : échec sur un répertoire vide (« Aucun fichier markdown trouvé dans ce dossier ») au lieu de répondre | 🟢 corrigé | P1 | 📱 frontend + ⚙️ backend | IA | `backend/bookslm_routes.py`, `backend/bookslm.py`, `frontend/js/bookslm.js` | Ouvrir l'assistant sur un dossier vide puis envoyer une question | `_resolve_system_prompt` dégrade vers le prompt Général + bloc « Dossier vide » (plus de 404) ; contexte applicatif `app_context` enrichi (documents ouverts, répertoire, recherche, fichiers récents) | Le 404 bloquait toute la requête. Feature #88, fiche `docs/features/ai-app-context.md`. Tests : `tests/test_bookslm.py` (+3), `tests/frontend/ai.test.mjs` |
|
||||
| *BUG-042* | [🟡 IMPORTANT] Assistant IA : liens de fichiers non fiables (« File not found: ») — pas de règle déterministe nom / dossier / chemin | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Cliquer les liens de fichiers/dossiers dans une réponse de l'assistant | `_classifyPath` distingue `name` (copie presse-papiers) / `dir` (révélation arborescence) / `file` (ouverture) ; `_activatePath()` résout le chemin contre l'index du vault (exact → suffixe → basename unique) avant d'agir | Les liens morts ouvraient un fichier inexistant. Feature #88. Tests : `tests/frontend/ai.test.mjs` (+3) |
|
||||
| *BUG-042* | [🟡 IMPORTANT] Assistant IA : liens de fichiers non fiables (« File not found: ») — pas de règle déterministe nom / dossier / chemin | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Cliquer les liens de fichiers/dossiers dans une réponse de l'assistant (y compris noms avec espaces) | `_classifyPath` distingue `name` (copie presse-papiers) / `dir` (révélation arborescence) / `file` (ouverture) ; `_activatePath()` résout le chemin contre l'index du vault (exact → suffixe → basename unique) avant d'agir ; espaces pris en charge (markdown `<…>`/`%20`, code inline, mentions brutes confirmées par l'index + plus long suffixe aligné sur un mot) | Les liens morts ouvraient un fichier inexistant. Feature #88. Tests : `tests/frontend/ai.test.mjs` (+6) |
|
||||
|
||||
### TODOs techniques (améliorations / nouvelles tâches)
|
||||
|
||||
@@ -188,6 +188,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
||||
| 2026-09-13 | Revue sécurité statique (BUG-021 → BUG-040, roadmap #84 → #87) | Enregistrement | `docs/ISSUES_TODOLIST.md`, `docs/ROADMAP.md` | Revue statique 2026-09-13 consignée : XSS markdown (`escape=False`) + page de partage publique, brute-force MFA, traversal par préfixe `paths.py`, ReDoS, SSRF webhooks, cycle de vie des sessions, politique de mot de passe, races `users.json`, audits IP, rate-limit, indexation symlinks, recherche O(N), CSP. BUG-021→BUG-040 ouverts au registre ; roadmap #84→#87 (consolidation/sécurité, refonte architecturale, performance, CI/CD). | 🔴 ouvert (à traiter) |
|
||||
| 2026-09-13 | BUG-021 → BUG-034 (#84 phase 1) | Correction | `backend/services/sanitizer.py`, `backend/services/paths.py`, `backend/services/net.py`, `backend/services/regex_safety.py`, `backend/main.py`, `backend/search.py`, `backend/webhooks.py`, `backend/indexer.py`, `backend/ratelimit.py`, `backend/auth/{router,user_store,password,jwt_handler,middleware}.py`, `frontend/js/{auth,admin,sync}.js`, `tests/test_security_hardening.py`, `tests/test_auth_api.py` | Sanitizer XSS serveur (markdown + page de partage), rate-limit/lockout MFA, isolation vaults par segments, caps regex (ReDoS), SSRF webhooks + secrets externalisés, rotation/révocation des jetons, politique de mot de passe + invalidation des sessions, verrous `users.json`, IP réelle dans les audits, rate-limit par compte, symlinks d'index ignorés, recherche simple via inverted index, token en mémoire + cookie HttpOnly + CSP durcie. Vérifié : pytest 961 passed / 6 skipped, ruff 0, mypy 0, tests frontend 36 modules + 9 JSDOM suites verts. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-13 | BUG-017 → BUG-020, #83 | Correction + feature | `frontend/js/mobile-editor.js`, `frontend/style.css`, `frontend/index.html`, `frontend/locales/{fr,en}.json`, `tests/frontend/mobile-editor.test.mjs`, `tests/e2e/mobile-editor.spec.js` | BUG-017 : cartes de tableau de bord compressibles (`min-width:0`) ; BUG-018 : en-tête éditeur mobile compact + barre IA tactile ; BUG-019 : ancrage du ruban via `visualViewport` ; BUG-020 : suppression de la double scrollbar. #83 : ruban d'édition horizontal défilable style Obsidian (commandes étendues + personnalisation ⚙ persistée). Vérifié : tests frontend 35/35 (mobile) + 50/50 (IA) + 9 JSDOM suites. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-14 | BUG-042 (complément) | Correction | `frontend/js/bookslm.js`, `tests/frontend/ai.test.mjs`, `docs/features/ai-app-context.md` | Prise en charge des **espaces** dans les noms de fichiers et chemins des liens de l'assistant : cibles markdown avec espaces / `<…>` / `%20` décodés, code inline reconnu (`_looksLikePath` élargi, rejette toujours les extraits de code), mentions brutes liées uniquement si présentes dans l'index du vault (`_linkifySpacePaths` + `_confirmPathInCache`, plus long suffixe aligné sur un mot pour ne pas avaler le mot de prose précédent). Vérifié : tests frontend 57/57 (IA) + 9 suites JSDOM, validate-imports 36 modules, pytest 963 passed / 6 skipped. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-14 | BUG-041, BUG-042, #88 | Correction + feature | `frontend/js/bookslm.js`, `backend/bookslm.py`, `backend/bookslm_routes.py`, `frontend/locales/{fr,en}.json`, `tests/test_bookslm.py`, `tests/frontend/ai.test.mjs`, `docs/features/ai-app-context.md` | BUG-041 : contexte de dossier vide → dégradation gracieuse vers le prompt Général + bloc « Dossier vide » (fin du 404). BUG-042 : liens de fichiers déterministes (nom → presse-papiers, dossier → arborescence, chemin → viewer) + résolution du chemin contre l'index du vault avant ouverture. #88 : `app_context` (documents ouverts, répertoire/vault courants, recherche + résultats affichés) et fichiers récemment modifiés injectés dans le prompt Général. Vérifié : pytest 963 passed / 6 skipped, ruff 0, mypy 0, tests frontend 52/52 + validate-imports 36 modules. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
|
||||
---
|
||||
|
||||
@@ -64,10 +64,28 @@ Avant d'agir, `_activatePath()` **résout** le chemin contre l'index du vault
|
||||
(segment de tête omis par le modèle), puis basename unique. Les chemins non
|
||||
résolus copient le nom au lieu d'ouvrir un lien mort — plus de `File not found`.
|
||||
|
||||
### 4. Espaces dans les noms de fichiers et les chemins (BUG-042)
|
||||
|
||||
Les noms de fichiers/dossiers pouvant contenir des espaces, la détection est
|
||||
adaptée :
|
||||
|
||||
- **Liens markdown** : la cible peut contenir des espaces, être encadrée par
|
||||
`<…>` ou porter un `"titre"` ; les URL percent-encodées (`%20`) sont décodées.
|
||||
- **Code inline** : `` `Ma note.md` `` / `` `Mon dossier/Ma note.md` `` sont
|
||||
reconnus (`_looksLikePath` accepte les espaces en gardant un jeu de caractères
|
||||
strict, ce qui rejette les extraits de code et commandes shell).
|
||||
- **Mentions brutes** : une mention avec espaces n'est liée **que si elle existe
|
||||
réellement dans l'index du vault** (`_linkifySpacePaths` +
|
||||
`_confirmPathInCache`), et l'acceptation se fait sur le **plus long suffixe
|
||||
aligné sur un mot** — ainsi « Ouvre Mon dossier/Ma note.md » ne crée pas de
|
||||
lien englobant « Ouvre » et la prose ordinaire n'est jamais liée à tort.
|
||||
|
||||
## Tests
|
||||
|
||||
- `tests/frontend/ai.test.mjs` : classification (`name`/`file`/`dir`), routage
|
||||
`_activatePath` (ouvre/révèle/copie), clic d'un lien « nom » → presse-papiers.
|
||||
`_activatePath` (ouvre/révèle/copie), clic d'un lien « nom » → presse-papiers,
|
||||
et prise en charge des espaces (code inline, liens markdown `%20`, mentions
|
||||
brutes liées uniquement si présentes dans l'index, résolution au clic).
|
||||
- `tests/test_bookslm.py` : `build_general_system_prompt` avec `app_context` +
|
||||
`recent_files` ; prompt inchangé sans contexte ; chat sur dossier vide ne
|
||||
renvoie plus 404.
|
||||
|
||||
+102
-12
@@ -23,6 +23,10 @@ export const MODE = Object.freeze({
|
||||
const FILE_EXT_RE = /\.(?:md|markdown|mdx|txt|pdf|excalidraw|canvas|png|jpe?g|gif|svg|webp|bmp|avif|csv|tsv|json|ya?ml|toml|ini|cfg|conf|html?|css|scss|sass|less|jsx?|tsx?|vue|svelte|py|rb|go|rs|java|kt|c|cc|cpp|h|hpp|cs|php|sh|bash|zsh|ps1|bat|sql|xml|log|tex|org|rtf|docx?|xlsx?|pptx?)$/i;
|
||||
// A path-like token: optional `dir/` segments followed by a final segment.
|
||||
const PATH_WITH_DIR_RE = /(^|[\s(>"'])((?:[\w.-]+\/)+[\w.-]+\.[A-Za-z0-9]{1,8})/g;
|
||||
// Same, but each segment may contain spaces (`Mon dossier/Ma note.md`). Matched
|
||||
// only when the candidate resolves against the vault path index so ordinary
|
||||
// prose is never turned into a link.
|
||||
const PATH_WITH_SPACES_RE = /(^|[\s(>"'\[])((?:[^\s/]+(?:[ \t]+[^\s/]+)*\/)+[^\s/]+(?:[ \t]+[^\s/]+)*\.[A-Za-z0-9]{1,8})/g;
|
||||
// Image files that can be attached to a vision-capable model.
|
||||
const IMAGE_EXT_RE = /\.(?:png|jpe?g|gif|svg|webp|bmp|avif|ico)$/i;
|
||||
// Resizable panel bounds (px).
|
||||
@@ -321,10 +325,77 @@ class BooksLM {
|
||||
|
||||
_normalizeLinkPath(rawPath) {
|
||||
return String(rawPath || '')
|
||||
.trim()
|
||||
.replace(/^\.\//, '')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\/+/, '')
|
||||
.replace(/\/+$/, '');
|
||||
.replace(/\/+$/, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous lookup of a (possibly space-containing) path in the cached vault
|
||||
* path index. Used at render time to decide whether a bare mention is a real
|
||||
* vault path before turning it into a link.
|
||||
*/
|
||||
_confirmPathInCache(rawPath) {
|
||||
const paths = this._pathCache;
|
||||
if (!Array.isArray(paths) || !paths.length) return null;
|
||||
const clean = this._normalizeLinkPath(rawPath).toLowerCase();
|
||||
if (!clean) return null;
|
||||
return paths.find((p) => String(p.path || '').toLowerCase() === clean)
|
||||
|| paths.find((p) => String(p.path || '').toLowerCase().endsWith('/' + clean))
|
||||
|| null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkify bare path mentions that contain spaces (`Mon dossier/Ma note.md`).
|
||||
*
|
||||
* A candidate is accepted only when the longest word-aligned suffix exists in
|
||||
* the vault path index. That keeps ordinary prose unturned-into-links and
|
||||
* prevents leading sentence words ("Ouvre Mon dossier/…") from being swallowed
|
||||
* into the link. Returns the segment unchanged when no path index is available.
|
||||
*/
|
||||
_linkifySpacePaths(segment, put) {
|
||||
const paths = this._pathCache;
|
||||
if (!Array.isArray(paths) || !paths.length) return segment;
|
||||
// Fresh regex instance: PATH_WITH_SPACES_RE is module-level and its
|
||||
// lastIndex must not leak between (possibly nested) render calls.
|
||||
const re = new RegExp(PATH_WITH_SPACES_RE.source, 'g');
|
||||
let out = '';
|
||||
let last = 0;
|
||||
let m;
|
||||
while ((m = re.exec(segment)) !== null) {
|
||||
const pre = m[1] || '';
|
||||
const cand = m[2];
|
||||
const candStart = m.index + pre.length;
|
||||
if (candStart < last) { re.lastIndex = candStart + 1; continue; }
|
||||
const accepted = this._acceptSpaceCandidate(this._decodeEntities(cand));
|
||||
if (!accepted) {
|
||||
// Rejected: retry from the next character so an over-long candidate
|
||||
// (leading prose word) does not mask the real path inside it.
|
||||
re.lastIndex = candStart + 1;
|
||||
continue;
|
||||
}
|
||||
out += segment.slice(last, candStart + accepted.offset);
|
||||
out += put(this._pathLink(accepted.display, accepted.path));
|
||||
last = candStart + cand.length;
|
||||
re.lastIndex = last;
|
||||
}
|
||||
return out ? out + segment.slice(last) : segment;
|
||||
}
|
||||
|
||||
/** Longest word-aligned suffix of `cand` that exists in the path index. */
|
||||
_acceptSpaceCandidate(cand) {
|
||||
const wordRe = /\S+/g;
|
||||
let w;
|
||||
while ((w = wordRe.exec(cand)) !== null) {
|
||||
const sub = cand.slice(w.index);
|
||||
if (!sub.includes('/')) continue;
|
||||
const hit = this._confirmPathInCache(sub);
|
||||
if (hit) return { path: hit.path, display: sub, offset: w.index };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2122,11 +2193,24 @@ class BooksLM {
|
||||
return put(`<code>${code}</code>`);
|
||||
});
|
||||
|
||||
// Explicit markdown links: [label](target).
|
||||
s = s.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_m, label, target) =>
|
||||
put(this._renderMdLink(label, target)));
|
||||
// Explicit markdown links: [label](target). The target may contain spaces
|
||||
// (`Mon dossier/Ma note.md`), an optional <angle-bracket> wrapper and an
|
||||
// optional "title" — all stripped before resolving.
|
||||
s = s.replace(/\[([^\]\n]+)\]\(([^)\n]*)\)/g, (_m, label, target) => {
|
||||
let linkTarget = String(target).trim();
|
||||
if (linkTarget.startsWith('<') && linkTarget.endsWith('>')) linkTarget = linkTarget.slice(1, -1).trim();
|
||||
linkTarget = linkTarget.replace(/\s+["'][^"']*["']$/, '').trim();
|
||||
if (!linkTarget) return _m;
|
||||
return put(this._renderMdLink(label, linkTarget));
|
||||
});
|
||||
|
||||
// Bare `dir/file.ext` mentions.
|
||||
// Bare mentions whose file/directory names contain spaces. Run BEFORE the
|
||||
// space-free pass so the full path is captured, and link only when the
|
||||
// candidate exists in the vault index — ordinary prose is never turned into
|
||||
// a dead link.
|
||||
s = this._linkifySpacePaths(s, put);
|
||||
|
||||
// Bare `dir/file.ext` mentions (no spaces).
|
||||
s = s.replace(PATH_WITH_DIR_RE, (_m, pre, path) =>
|
||||
`${pre}${put(this._pathLink(path, this._decodeEntities(path)))}`);
|
||||
|
||||
@@ -2213,13 +2297,17 @@ class BooksLM {
|
||||
if (isExternal) {
|
||||
return `<a href="${target}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
||||
}
|
||||
const raw = this._decodeEntities(target).replace(/^\.\//, '');
|
||||
return this._pathLink(label, raw);
|
||||
// Links emitted by models are sometimes percent-encoded (spaces as %20).
|
||||
let decoded = this._decodeEntities(target);
|
||||
if (/%[0-9a-f]{2}/i.test(decoded)) {
|
||||
try { decoded = decodeURIComponent(decoded); } catch { /* keep as-is */ }
|
||||
}
|
||||
return this._pathLink(label, decoded);
|
||||
}
|
||||
|
||||
/** Build a clickable link for a vault-relative file or directory path. */
|
||||
_pathLink(display, rawPath) {
|
||||
const clean = String(rawPath || '').replace(/^\.\//, '');
|
||||
const clean = this._normalizeLinkPath(this._decodeEntities(rawPath));
|
||||
const kind = this._classifyPath(clean);
|
||||
const attr = this._escapeAttr(clean);
|
||||
return `<a href="#" class="bookslm-path-link" data-path="${attr}" data-kind="${kind}" title="${attr}">${display}</a>`;
|
||||
@@ -2234,10 +2322,12 @@ class BooksLM {
|
||||
}
|
||||
|
||||
_looksLikePath(value) {
|
||||
if (!value) return false;
|
||||
const p = value.trim();
|
||||
if (!p || /\s/.test(p)) return false;
|
||||
if (!/^[\w./\\-]+$/.test(p)) return false;
|
||||
if (value == null) return false;
|
||||
const p = String(value).trim();
|
||||
if (!p || /[\n\r]/.test(p)) return false;
|
||||
// Allow spaces inside file/directory names (`Ma note.md`), but keep the
|
||||
// character set tight so prose and code snippets are not mistaken for paths.
|
||||
if (!/^[\w./\\ ()'&-]+$/.test(p)) return false;
|
||||
return p.includes('/') || FILE_EXT_RE.test(p);
|
||||
}
|
||||
|
||||
|
||||
@@ -415,6 +415,65 @@ async function main() {
|
||||
assert.equal(b._classifyPath("projets/2026"), "dir", "dir path → reveal");
|
||||
});
|
||||
|
||||
// ── 12b. Spaces in file names / paths ──
|
||||
await test("_looksLikePath accepts spaced names but rejects code snippets", () => {
|
||||
const b = new BooksLM();
|
||||
assert.equal(b._looksLikePath("Ma note.md"), true, "spaced filename is a path");
|
||||
assert.equal(b._looksLikePath("Mon dossier/Ma note.md"), true, "spaced path is a path");
|
||||
assert.equal(b._looksLikePath("const x = 1;"), false, "code snippet rejected");
|
||||
assert.equal(b._looksLikePath("npm run test"), false, "shell command rejected");
|
||||
});
|
||||
|
||||
await test("_renderMarkdown linkifies inline code containing spaces", () => {
|
||||
const b = new BooksLM();
|
||||
const html = b._renderMarkdown("Voir `Mon dossier/Ma note.md` pour le détail.");
|
||||
assert.ok(html.includes('data-path="Mon dossier/Ma note.md"'), "spaced path kept intact");
|
||||
assert.ok(html.includes('data-kind="file"'), "spaced file classified as file");
|
||||
});
|
||||
|
||||
await test("_renderMarkdown linkifies markdown links with spaces and %20", () => {
|
||||
const b = new BooksLM();
|
||||
const spaced = b._renderMarkdown("[Ma note](Mon dossier/Ma note.md)");
|
||||
assert.ok(spaced.includes('data-path="Mon dossier/Ma note.md"'), "raw spaced target kept");
|
||||
|
||||
const encoded = b._renderMarkdown("[Ma note](Mon%20dossier/Ma%20note.md)");
|
||||
assert.ok(encoded.includes('data-path="Mon dossier/Ma note.md"'), "%20 decoded to space");
|
||||
});
|
||||
|
||||
await test("bare spaced paths link only when present in the vault index", () => {
|
||||
const b = new BooksLM();
|
||||
|
||||
// No index → no link (prose must not become a dead link).
|
||||
b._pathCache = [];
|
||||
const noCache = b._renderMarkdown("Ouvre Mon dossier/Ma note.md pour la suite");
|
||||
assert.ok(!noCache.includes("bookslm-path-link"), "no link without index");
|
||||
|
||||
// Indexed path → link, and the leading prose word is not swallowed.
|
||||
b._pathCache = [{ path: "Mon dossier/Ma note.md", name: "Ma note.md", type: "file" }];
|
||||
const html = b._renderMarkdown("Ouvre Mon dossier/Ma note.md pour la suite");
|
||||
assert.ok(html.includes('data-path="Mon dossier/Ma note.md"'), "indexed spaced path linkified");
|
||||
assert.ok(html.includes(">Mon dossier/Ma note.md</a>"), "link text excludes leading word");
|
||||
assert.ok(html.includes("Ouvre Mon") === false || !html.includes(">Ouvre Mon"), "prose word kept outside the link");
|
||||
});
|
||||
|
||||
await test("clicking a spaced file link resolves against the index", async () => {
|
||||
const b = new BooksLM();
|
||||
b._activeVault = () => "TestVault";
|
||||
const opened = [];
|
||||
const revealed = [];
|
||||
b._openFileLink = (p) => { opened.push(p); return Promise.resolve(); };
|
||||
b._revealPath = (p) => { revealed.push(p); return Promise.resolve(); };
|
||||
b._ensurePaths = async () => ([
|
||||
{ path: "Mon dossier/Ma note.md", name: "Ma note.md", type: "file" },
|
||||
{ path: "Mon dossier/Sous dossier", name: "Sous dossier", type: "directory" },
|
||||
]);
|
||||
|
||||
await b._activatePath("Ma note.md", "file");
|
||||
await b._activatePath("Sous dossier", "dir");
|
||||
assert.deepEqual(opened, ["Mon dossier/Ma note.md"], "spaced filename resolves to its real path");
|
||||
assert.deepEqual(revealed, ["Mon dossier/Sous dossier"], "spaced directory resolves too");
|
||||
});
|
||||
|
||||
await test("_renderMarkdown keeps external links external", () => {
|
||||
const b = new BooksLM();
|
||||
const html = b._renderMarkdown("[site](https://example.com/page)");
|
||||
|
||||
Reference in New Issue
Block a user