Files
ObsiGate/frontend/js/autocomplete.js
T
bruno 2508c37ef2
CI / lint (push) Failing after 19s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Failing after 10s
fix: 422 error on normal refresh — min query length violation
- Changed q=* to q=** in autocomplete.js and offline.js
- Backend requires min_query_length=2, single * was 1 char → 422
- Caused blank page on F5 refresh (non-hard-refresh)
2026-06-19 15:16:11 -04:00

351 lines
15 KiB
JavaScript

/* ObsiGate — Autocomplete intelligent
* Partagé entre éditeur Forge (textarea) et CodeMirror.
* Détecte le contexte autour du curseur et suggère des complétions.
*
* Contexte supporté : mermaid, code fence, frontmatter, markdown, wikilink, table, texte libre.
*
* API :
* suggest(text, cursorPos) → { context, items: [{label, detail, insert, from, to}] }
* CompletionSource pour CodeMirror : codeMirrorCompletionSource(context)
*/
// ── Suggestion Data ──────────────────────────────────────────────────
var MERMAID_TYPES = [
{ label: 'flowchart TD', detail: 'Diagramme de flux (top-down)', insert: 'flowchart TD\n ' },
{ label: 'flowchart LR', detail: 'Diagramme de flux (gauche-droite)', insert: 'flowchart LR\n ' },
{ label: 'sequenceDiagram', detail: 'Sequence (acteurs + messages)', insert: 'sequenceDiagram\n ' },
{ label: 'classDiagram', detail: 'Diagramme de classes UML', insert: 'classDiagram\n ' },
{ label: 'stateDiagram-v2', detail: 'Diagramme d\'etats', insert: 'stateDiagram-v2\n ' },
{ label: 'erDiagram', detail: 'Diagramme Entite-Relation', insert: 'erDiagram\n ' },
{ label: 'journey', detail: 'Parcours utilisateur', insert: 'journey\n ' },
{ label: 'gantt', detail: 'Diagramme de Gantt', insert: 'gantt\n ' },
{ label: 'pie', detail: 'Diagramme circulaire', insert: 'pie\n ' },
{ label: 'gitGraph', detail: 'Historique Git', insert: 'gitGraph\n ' },
{ label: 'mindmap', detail: 'Carte mentale', insert: 'mindmap\n ' },
{ label: 'timeline', detail: 'Ligne du temps', insert: 'timeline\n ' },
{ label: 'quadrantChart', detail: 'Graphique quadrants', insert: 'quadrantChart\n ' },
{ label: 'xychart-beta', detail: 'Graphique XY', insert: 'xychart-beta\n ' },
{ label: 'block-beta', detail: 'Diagramme de blocs', insert: 'block-beta\n ' }
];
var MERMAID_SYNTAX = {
'flowchart': [
{ label: 'A-->B', detail: 'Fleche', insert: '-->' },
{ label: 'A--text-->B', detail: 'Fleche avec texte', insert: '-- texte -->' },
{ label: 'A-.->B', detail: 'Fleche pointillee', insert: '-.->' },
{ label: 'A==>B', detail: 'Fleche epaisse', insert: '==>' },
{ label: 'subgraph', detail: 'Sous-graphe', insert: 'subgraph titre\n \nend' },
{ label: 'style A fill:#xxx', detail: 'Style de noeud', insert: 'style A fill:#333,stroke:#58a6ff' },
{ label: 'classDef', detail: 'Classe CSS', insert: 'classDef default fill:#333,stroke:#58a6ff' }
],
'sequenceDiagram': [
{ label: 'participant X', detail: 'Declarer acteur', insert: 'participant Nom' },
{ label: 'A->>B: msg', detail: 'Message', insert: '->> : ' },
{ label: 'A-->>B: msg', detail: 'Message asynchrone', insert: '-->> : ' },
{ label: 'Note over A,B', detail: 'Note', insert: 'Note over A,B: ' },
{ label: 'loop text', detail: 'Boucle', insert: 'loop description\n \nend' },
{ label: 'alt/else', detail: 'Alternative', insert: 'alt condition\n \nelse\n \nend' },
{ label: 'activate A', detail: 'Activer ligne de vie', insert: 'activate A' }
],
'classDiagram': [
{ label: 'class Nom {', detail: 'Declarer classe', insert: 'class Nom {\n \n}' },
{ label: '+methode()', detail: 'Methode publique', insert: '+nom() Type' },
{ label: '-attribut', detail: 'Attribut prive', insert: '-nom Type' },
{ label: 'Heritage', detail: 'Relation heritage', insert: ' <|-- ' },
{ label: 'Composition', detail: 'Relation composition', insert: ' *-- ' }
],
'gantt': [
{ label: 'dateFormat', detail: 'Format de date', insert: 'dateFormat YYYY-MM-DD' },
{ label: 'section', detail: 'Section Gantt', insert: 'section Nom' },
{ label: 'tache : apres', detail: 'Tache avec dependance', insert: ' : after tache1, 3d' }
],
'pie': [
{ label: 'title', detail: 'Titre du pie', insert: 'title Titre du graphique' },
{ label: '"Label" : val', detail: 'Valeur', insert: '"Label" : 50' }
],
'mindmap': [
{ label: 'root', detail: 'Racine', insert: 'root((Sujet))' },
{ label: 'branche', detail: 'Sous-sujet', insert: '\n Sous-sujet' },
{ label: 'leaf', detail: 'Feuille', insert: '\n Detail' }
]
};
var CODE_LANGUAGES = [
{ label: 'python', detail: 'Python', insert: 'python' },
{ label: 'javascript', detail: 'JavaScript', insert: 'javascript' },
{ label: 'typescript', detail: 'TypeScript', insert: 'typescript' },
{ label: 'bash', detail: 'Bash / Shell', insert: 'bash' },
{ label: 'sql', detail: 'SQL', insert: 'sql' },
{ label: 'json', detail: 'JSON', insert: 'json' },
{ label: 'yaml', detail: 'YAML', insert: 'yaml' },
{ label: 'html', detail: 'HTML', insert: 'html' },
{ label: 'css', detail: 'CSS', insert: 'css' },
{ label: 'dockerfile', detail: 'Dockerfile', insert: 'dockerfile' },
{ label: 'mermaid', detail: 'Mermaid (diagrammes)', insert: 'mermaid' },
{ label: 'markdown', detail: 'Markdown', insert: 'markdown' },
{ label: 'rust', detail: 'Rust', insert: 'rust' },
{ label: 'go', detail: 'Go', insert: 'go' },
{ label: 'java', detail: 'Java', insert: 'java' }
];
var CODE_SNIPPETS = {
python: [
{ label: 'def', detail: 'Fonction', insert: 'def nom(params):\n ' },
{ label: 'class', detail: 'Classe', insert: 'class Nom:\n def __init__(self):\n ' },
{ label: 'if/else', detail: 'Condition', insert: 'if condition:\n \nelse:\n ' },
{ label: 'for', detail: 'Boucle for', insert: 'for item in iterable:\n ' },
{ label: 'try/except', detail: 'Try-except', insert: 'try:\n \nexcept Exception as e:\n ' },
{ label: 'import', detail: 'Import', insert: 'import ' },
{ label: 'print', detail: 'Print', insert: 'print()' },
{ label: 'lambda', detail: 'Lambda', insert: 'lambda x: ' }
],
javascript: [
{ label: 'function', detail: 'Fonction', insert: 'function nom(params) {\n \n}' },
{ label: 'const', detail: 'Constante', insert: 'const nom = ' },
{ label: '=>', detail: 'Fleche', insert: '(params) => {\n \n}' },
{ label: 'if/else', detail: 'Condition', insert: 'if (cond) {\n \n} else {\n \n}' },
{ label: 'for', detail: 'Boucle for', insert: 'for (let i = 0; i < n; i++) {\n \n}' },
{ label: 'try/catch', detail: 'Try-catch', insert: 'try {\n \n} catch (e) {\n \n}' },
{ label: 'console.log', detail: 'Console log', insert: 'console.log()' },
{ label: 'async/await', detail: 'Async function', insert: 'async function nom() {\n await \n}' }
],
bash: [
{ label: '#!/bin/bash', detail: 'Shebang', insert: '#!/bin/bash' },
{ label: 'if/fi', detail: 'Condition', insert: 'if [ condition ]; then\n \nfi' },
{ label: 'for', detail: 'Boucle for', insert: 'for item in list; do\n \ndone' },
{ label: 'case/esac', detail: 'Case', insert: 'case var in\n pattern)\n ;;\nesac' },
{ label: 'function', detail: 'Fonction', insert: 'nom() {\n \n}' }
],
sql: [
{ label: 'SELECT', detail: 'Select', insert: 'SELECT colonnes\nFROM table' },
{ label: 'JOIN', detail: 'Join', insert: 'JOIN table2 ON table1.id = table2.id' },
{ label: 'WHERE', detail: 'Where', insert: 'WHERE condition' },
{ label: 'GROUP BY', detail: 'Group by', insert: 'GROUP BY colonne' },
{ label: 'ORDER BY', detail: 'Order by', insert: 'ORDER BY colonne DESC' },
{ label: 'INSERT INTO', detail: 'Insert', insert: 'INSERT INTO table (cols) VALUES (vals)' },
{ label: 'CREATE TABLE', detail: 'Create table', insert: 'CREATE TABLE nom (\n id INTEGER PRIMARY KEY,\n \n)' }
]
};
var FRONTMATTER_FIELDS = [
{ label: 'title:', detail: 'Titre du document', insert: 'title: ' },
{ label: 'tags:', detail: 'Tags (liste)', insert: 'tags:\n - ' },
{ label: 'date:', detail: 'Date', insert: 'date: ' + new Date().toISOString().slice(0,10) },
{ label: 'status:', detail: 'Statut', insert: 'status: en cours' },
{ label: 'category:', detail: 'Categorie', insert: 'category: ' },
{ label: 'publish:', detail: 'Publier (true/false)', insert: 'publish: true' },
{ label: 'favoris:', detail: 'Favoris (true/false)', insert: 'favoris: true' },
{ label: 'author:', detail: 'Auteur', insert: 'author: ' },
{ label: 'description:', detail: 'Description', insert: 'description: ' }
];
var MARKDOWN_FORMAT = [
{ label: '# H1', detail: 'Titre niveau 1', insert: '# ' },
{ label: '## H2', detail: 'Titre niveau 2', insert: '## ' },
{ label: '### H3', detail: 'Titre niveau 3', insert: '### ' },
{ label: '**gras**', detail: 'Texte en gras', insert: '****' },
{ label: '*italique*', detail: 'Texte en italique', insert: '**' },
{ label: '`code`', detail: 'Code inline', insert: '``' },
{ label: '[lien](url)', detail: 'Lien', insert: '[]()' },
{ label: '![image](url)', detail: 'Image', insert: '![]()' },
{ label: '- liste', detail: 'Liste a puces', insert: '- ' },
{ label: '1. liste', detail: 'Liste numerotee', insert: '1. ' },
{ label: '> citation', detail: 'Citation', insert: '> ' },
{ label: '---', detail: 'Separateur horizontal', insert: '\n---\n' },
{ label: '> [!note]', detail: 'Callout Note', insert: '> [!note] Titre\n> Contenu' },
{ label: '> [!warning]', detail: 'Callout Warning', insert: '> [!warning] Titre\n> Contenu' },
{ label: '> [!tip]', detail: 'Callout Tip', insert: '> [!tip] Titre\n> Contenu' },
{ label: '> [!danger]', detail: 'Callout Danger', insert: '> [!danger] Titre\n> Contenu' }
];
// ── Context Detection ─────────────────────────────────────────────────
function detectContext(text, cursorPos) {
var before = text.slice(0, cursorPos);
var after = text.slice(cursorPos);
var lines = before.split('\n');
var currentLine = lines[lines.length - 1] || '';
var lineNum = lines.length;
// Check for code fence context (look backwards for unclosed ```)
var inFence = null;
var fenceLang = '';
var fenceStart = 0;
for (var i = lines.length - 1; i >= 0; i--) {
var l = lines[i].trim();
var m = l.match(/^```(\w*)/);
if (m) {
if (inFence === null) {
inFence = true;
fenceLang = m[1] || '';
fenceStart = i + 1;
} else {
inFence = false;
break;
}
}
}
if (inFence) {
if (fenceLang === 'mermaid') return { type: 'mermaid', lang: '', line: currentLine };
return { type: 'code', lang: fenceLang, line: currentLine };
}
// Check for frontmatter context (between first and second ---)
if (lineNum >= 2) {
var firstSep = -1, secondSep = -1;
for (var j = 0; j < lines.length; j++) {
if (lines[j].trim() === '---') {
if (firstSep === -1) firstSep = j;
else if (secondSep === -1) { secondSep = j; break; }
}
}
if (firstSep === 0 && secondSep === -1) {
return { type: 'frontmatter', line: currentLine };
}
}
// Check for wikilink
if (/\[\[$/.test(currentLine) || /\[\[[^\]]*$/.test(currentLine)) {
var partial = (currentLine.match(/\[\[([^\]]*)$/) || ['',''])[1];
return { type: 'wikilink', partial: partial, line: currentLine };
}
// Check for markdown link
if (/\[.*\]\($/.test(currentLine)) {
return { type: 'link', line: currentLine };
}
// Check for triple backtick (about to open fence)
if (/```$/.test(before) || /^```\s*$/.test(currentLine)) {
return { type: 'fence_open', line: currentLine };
}
// Check if inside a markdown table
if (/^\|.*\|$/.test(currentLine.trim())) {
return { type: 'table', line: currentLine };
}
// Plain text / markdown
return { type: 'markdown', line: currentLine };
}
// ── Suggestion Engine ─────────────────────────────────────────────────
function suggest(text, cursorPos) {
var ctx = detectContext(text, cursorPos);
var items = [];
var prefix = '';
var currentLine = ctx.line;
switch (ctx.type) {
case 'mermaid':
// Try to match mermaid type already declared
var mermaidMatch = text.match(/```mermaid\s*\n(\w+)/);
if (mermaidMatch) {
var mmType = mermaidMatch[1];
items = MERMAID_SYNTAX[mmType] || [];
}
if (items.length === 0) {
items = MERMAID_TYPES;
}
break;
case 'code':
items = CODE_SNIPPETS[ctx.lang] || [];
break;
case 'fence_open':
items = CODE_LANGUAGES;
break;
case 'frontmatter':
prefix = currentLine.trim();
items = FRONTMATTER_FIELDS.filter(function(f) {
return f.label.startsWith(prefix) || prefix === '';
}).map(function(f) {
return { label: f.label, detail: f.detail, insert: prefix ? f.insert : f.insert };
});
return {
context: ctx.type,
items: items.length > 0 ? items : FRONTMATTER_FIELDS.map(function(f) {
return { label: f.label, detail: f.detail, insert: f.insert };
})
};
case 'wikilink':
return {
context: 'wikilink',
items: [],
partial: ctx.partial,
async: true // requires file index lookup
};
case 'markdown':
default:
// At start of line: markdown formatting
if (currentLine.trim().length < 3) {
items = MARKDOWN_FORMAT;
} else {
items = [];
}
break;
}
var filtered = items.filter(function(item) {
return !prefix || item.label.toLowerCase().indexOf(prefix.toLowerCase()) !== -1;
});
return {
context: ctx.type,
items: filtered.length > 0 ? filtered : items
};
}
// ── Wikilink async completion (fetches file list from server) ─────────
var _fileCache = null;
var _fileCacheTime = 0;
function searchWikilinks(partial, callback) {
var now = Date.now();
// Use cached file list if fresh (< 30s)
if (_fileCache && (now - _fileCacheTime) < 30000) {
callback(filterFiles(partial));
return;
}
try {
fetch('/api/search/advanced?q=**&vault=all&limit=500')
.then(function(r) { return r.json(); })
.then(function(data) {
_fileCache = (data.results || []).map(function(r) {
return {
path: r.path,
title: r.title || r.path.replace(/\.md$/i, ''),
vault: r.vault,
name: r.path.split('/').pop().replace(/\.md$/i, '')
};
});
_fileCacheTime = now;
callback(filterFiles(partial));
})
.catch(function() { callback([]); });
} catch(e) { callback([]); }
}
function filterFiles(partial) {
if (!_fileCache) return [];
var q = (partial || '').toLowerCase();
return _fileCache.filter(function(f) {
return f.name.toLowerCase().indexOf(q) !== -1 || f.path.toLowerCase().indexOf(q) !== -1;
}).slice(0, 20).map(function(f) {
return {
label: f.name,
detail: f.path,
insert: f.path + ']]',
vault: f.vault
};
});
}
export { suggest, searchWikilinks, detectContext, MERMAID_TYPES, CODE_LANGUAGES, FRONTMATTER_FIELDS, MARKDOWN_FORMAT };