mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 23:18:13 +02:00
Formules LaTeX rendues dans les réponses (KaTeX, repris d'AJEAN #123)
00-math.js : $…$, $$…$$, \(…\) et \[…\] mis de côté avant marked (qui abîmerait \, _ et *), remplacés par un repère puis par le rendu KaTeX. KaTeX n'est chargé qu'à la première formule ; d'ici là la source s'affiche, puis est rendue sur place. Le code (blocs et en ligne) et les montants (« 5 $ et 10 $ », « $5 et $10 », \$) restent du texte. Logique de détection d'AJEAN, avec deux garde-fous en plus : un $ ouvrant précédé d'un chiffre et d'une espace est un montant (« 5 $, à 10$ »), et $…$ ne contient pas de backtick — sans quoi « 5 $, code `$x$` » s'appariait par-dessus le code en ligne (le cas passe aussi chez AJEAN). Fonctions pures dans un fichier à part : web_katex_math_test.go les exécute sous node (ignoré sans node) sur les cas limites, et de bout en bout avec marked. index.html régénéré par tools/assemble-ui ; NOTICE.md mentionne la reprise et la licence MIT de KaTeX. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99c96a6c33
commit
965a4d6247
6 files changed
+320
-2
No files matched your search
@@ -1418,6 +1418,10 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
||||
border:1px solid var(--border);border-radius:999px;background:var(--panel);
|
||||
color:var(--dim);font-size:12px;line-height:1.5}
|
||||
.stopnote::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dim)}
|
||||
/* Formules LaTeX (KaTeX, 00-math.js) : une longue formule défile au lieu de déborder. */
|
||||
.katex{font-size:1.08em}
|
||||
.katex-display{margin:.6em 0;overflow-x:auto;overflow-y:hidden;padding:2px 0}
|
||||
.math-pending{font-family:var(--mono);font-size:.92em;color:var(--dim)}
|
||||
|
||||
/* ===== Fenêtres et sélecteurs =====================================
|
||||
Coins arrondis et ombre douce plutôt qu'un cadre marqué. */
|
||||
@@ -3820,6 +3824,76 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
</div>
|
||||
<script src="marked.min.js"></script>
|
||||
<script>
|
||||
// --- Formules LaTeX (KaTeX) ---------------------------------------------------
|
||||
// Repris d'AJEAN (#123). md() (00-md.js) met les formules de côté AVANT marked,
|
||||
// qui abîmerait \\, _ et * : un repère (caractères à usage privé U+E000/U+E001
|
||||
// autour du numéro) prend leur place, puis reçoit le rendu. Le code (blocs et
|
||||
// `en ligne`) n'est jamais touché. Formes reconnues : $$…$$ et \[…\] (bloc),
|
||||
// \(…\) et $…$ (en ligne). $…$ n'est une formule que s'il ne commence ni ne finit
|
||||
// par une espace, n'est pas collé à un mot ni échappé (\$) avant, et n'est pas
|
||||
// suivi d'un chiffre : « 5 $ et 10 $ » ou « $5 et $10 » restent du texte.
|
||||
// Ajouts de Loki : un $ ouvrant précédé d'un chiffre et d'une espace (« 5 $, »)
|
||||
// est un montant, et $…$ ne contient pas de ` — sans quoi « 5 $, code `$x$` »
|
||||
// s'apparierait par-dessus le code en ligne.
|
||||
// KaTeX (embarqué, servi sous /katex/ par web_katex.go et la façade) n'est chargé
|
||||
// qu'à la première formule ; d'ici là la source s'affiche, puis est rendue sur
|
||||
// place. (Pas de lookbehind dans l'expression : les iOS d'avant 16.4 refusent de
|
||||
// charger le script entier. Ces contrôles-là sont faits dans le remplacement.)
|
||||
//
|
||||
// MATH_RE et mathExtract sont purs (ni DOM ni réseau) : web_katex_math_test.go les
|
||||
// exécute sous node sur les cas limites (montants, code, échappements).
|
||||
const MATH_RE = /(```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)|`[^`\n]*`)|\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)|\$(?![\s$])((?:\\.|[^$\\\n`])+?)\$(?![\d$])/g;
|
||||
const MATH_MARK_RE = /\uE000(\d+)\uE001/g;
|
||||
function mathExtract(s){
|
||||
const list = [];
|
||||
const text = s.replace(MATH_RE, (all, code, dd, br, pa, d, at)=>{
|
||||
if(code !== undefined) return all;
|
||||
// $…$ : pas collé à un mot ni échappé (\$) avant, pas un montant (« 5 $ »),
|
||||
// pas d'espace avant le $ final.
|
||||
if(d !== undefined && (/[\\$\w]/.test(s[at-1] || '') || /\d\s$/.test(s.slice(Math.max(0, at-2), at)) || /\s$/.test(d))) return all;
|
||||
const display = dd !== undefined || br !== undefined;
|
||||
list.push({tex: (dd ?? br ?? pa ?? d).trim(), display});
|
||||
return '\uE000' + (list.length-1) + '\uE001';
|
||||
});
|
||||
return {text, list};
|
||||
}
|
||||
// mathRestore remplace chaque repère du HTML produit par marked par le rendu de
|
||||
// sa formule (render : formule → HTML).
|
||||
function mathRestore(html, list, render){
|
||||
return list.length ? html.replace(MATH_MARK_RE, (all, i)=>list[+i] ? render(list[+i]) : all) : html;
|
||||
}
|
||||
const mathEsc = (s)=>s.replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||||
const MATH_OPTS = {throwOnError: false, output: 'html'};
|
||||
function mathHtml(f){
|
||||
if(window.katex){
|
||||
try{ return katex.renderToString(f.tex, {...MATH_OPTS, displayMode: f.display}); }
|
||||
catch(_){ return '<code>'+mathEsc(f.tex)+'</code>'; }
|
||||
}
|
||||
mathLoad();
|
||||
return '<span class="math-pending" data-d="'+(f.display?1:0)+'">'+mathEsc(f.tex)+'</span>';
|
||||
}
|
||||
let mathLoading = null;
|
||||
function mathLoad(){
|
||||
if(mathLoading) return mathLoading;
|
||||
if(!document.querySelector('link[data-katex]')){
|
||||
const css = document.createElement('link');
|
||||
css.rel = 'stylesheet'; css.href = 'katex/katex.min.css'; css.dataset.katex = '1';
|
||||
document.head.appendChild(css);
|
||||
}
|
||||
mathLoading = new Promise((ok, ko)=>{
|
||||
const s = document.createElement('script');
|
||||
s.src = 'katex/katex.min.js'; s.onload = ok;
|
||||
// Échec (hors ligne, 404) : script retiré, nouvel essai à la prochaine formule.
|
||||
s.onerror = ()=>{ s.remove(); ko(); };
|
||||
document.head.appendChild(s);
|
||||
}).then(()=>{
|
||||
document.querySelectorAll('.math-pending').forEach(el=>{
|
||||
try{ katex.render(el.textContent, el, {...MATH_OPTS, displayMode: el.dataset.d === '1'}); }catch(_){}
|
||||
el.classList.remove('math-pending');
|
||||
});
|
||||
}).catch(()=>{ mathLoading = null; });
|
||||
return mathLoading;
|
||||
}
|
||||
// marked: GFM on, no auto-breaks (single newlines stay inline).
|
||||
marked.setOptions({ gfm: true, breaks: false });
|
||||
// Pre-process to fix common LLM markdown mistakes before handing to marked:
|
||||
@@ -3836,7 +3910,13 @@ function fixMd(s){
|
||||
s = s.replace(/\n{3,}/g, '\n\n');
|
||||
return s;
|
||||
}
|
||||
function md(src){ return src ? marked.parse(fixMd(src)) : ''; }
|
||||
// Les formules LaTeX sont mises de côté avant marked puis rendues par KaTeX
|
||||
// (00-math.js) ; le code et les montants en $ ne sont pas touchés.
|
||||
function md(src){
|
||||
if(!src) return '';
|
||||
const m = mathExtract(fixMd(src));
|
||||
return mathRestore(marked.parse(m.text), m.list, mathHtml);
|
||||
}
|
||||
// Les garde-fous du serveur ("[stop: trop d'appels d'outils]") sont concaténés
|
||||
// au texte de la réponse : ils arrivent donc en clair, au milieu du markdown.
|
||||
// markNotices les sort du fil pour qu'on ne les prenne pas pour une phrase du
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// --- Formules LaTeX (KaTeX) ---------------------------------------------------
|
||||
// Repris d'AJEAN (#123). md() (00-md.js) met les formules de côté AVANT marked,
|
||||
// qui abîmerait \\, _ et * : un repère (caractères à usage privé U+E000/U+E001
|
||||
// autour du numéro) prend leur place, puis reçoit le rendu. Le code (blocs et
|
||||
// `en ligne`) n'est jamais touché. Formes reconnues : $$…$$ et \[…\] (bloc),
|
||||
// \(…\) et $…$ (en ligne). $…$ n'est une formule que s'il ne commence ni ne finit
|
||||
// par une espace, n'est pas collé à un mot ni échappé (\$) avant, et n'est pas
|
||||
// suivi d'un chiffre : « 5 $ et 10 $ » ou « $5 et $10 » restent du texte.
|
||||
// Ajouts de Loki : un $ ouvrant précédé d'un chiffre et d'une espace (« 5 $, »)
|
||||
// est un montant, et $…$ ne contient pas de ` — sans quoi « 5 $, code `$x$` »
|
||||
// s'apparierait par-dessus le code en ligne.
|
||||
// KaTeX (embarqué, servi sous /katex/ par web_katex.go et la façade) n'est chargé
|
||||
// qu'à la première formule ; d'ici là la source s'affiche, puis est rendue sur
|
||||
// place. (Pas de lookbehind dans l'expression : les iOS d'avant 16.4 refusent de
|
||||
// charger le script entier. Ces contrôles-là sont faits dans le remplacement.)
|
||||
//
|
||||
// MATH_RE et mathExtract sont purs (ni DOM ni réseau) : web_katex_math_test.go les
|
||||
// exécute sous node sur les cas limites (montants, code, échappements).
|
||||
const MATH_RE = /(```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)|`[^`\n]*`)|\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)|\$(?![\s$])((?:\\.|[^$\\\n`])+?)\$(?![\d$])/g;
|
||||
const MATH_MARK_RE = /\uE000(\d+)\uE001/g;
|
||||
function mathExtract(s){
|
||||
const list = [];
|
||||
const text = s.replace(MATH_RE, (all, code, dd, br, pa, d, at)=>{
|
||||
if(code !== undefined) return all;
|
||||
// $…$ : pas collé à un mot ni échappé (\$) avant, pas un montant (« 5 $ »),
|
||||
// pas d'espace avant le $ final.
|
||||
if(d !== undefined && (/[\\$\w]/.test(s[at-1] || '') || /\d\s$/.test(s.slice(Math.max(0, at-2), at)) || /\s$/.test(d))) return all;
|
||||
const display = dd !== undefined || br !== undefined;
|
||||
list.push({tex: (dd ?? br ?? pa ?? d).trim(), display});
|
||||
return '\uE000' + (list.length-1) + '\uE001';
|
||||
});
|
||||
return {text, list};
|
||||
}
|
||||
// mathRestore remplace chaque repère du HTML produit par marked par le rendu de
|
||||
// sa formule (render : formule → HTML).
|
||||
function mathRestore(html, list, render){
|
||||
return list.length ? html.replace(MATH_MARK_RE, (all, i)=>list[+i] ? render(list[+i]) : all) : html;
|
||||
}
|
||||
const mathEsc = (s)=>s.replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||||
const MATH_OPTS = {throwOnError: false, output: 'html'};
|
||||
function mathHtml(f){
|
||||
if(window.katex){
|
||||
try{ return katex.renderToString(f.tex, {...MATH_OPTS, displayMode: f.display}); }
|
||||
catch(_){ return '<code>'+mathEsc(f.tex)+'</code>'; }
|
||||
}
|
||||
mathLoad();
|
||||
return '<span class="math-pending" data-d="'+(f.display?1:0)+'">'+mathEsc(f.tex)+'</span>';
|
||||
}
|
||||
let mathLoading = null;
|
||||
function mathLoad(){
|
||||
if(mathLoading) return mathLoading;
|
||||
if(!document.querySelector('link[data-katex]')){
|
||||
const css = document.createElement('link');
|
||||
css.rel = 'stylesheet'; css.href = 'katex/katex.min.css'; css.dataset.katex = '1';
|
||||
document.head.appendChild(css);
|
||||
}
|
||||
mathLoading = new Promise((ok, ko)=>{
|
||||
const s = document.createElement('script');
|
||||
s.src = 'katex/katex.min.js'; s.onload = ok;
|
||||
// Échec (hors ligne, 404) : script retiré, nouvel essai à la prochaine formule.
|
||||
s.onerror = ()=>{ s.remove(); ko(); };
|
||||
document.head.appendChild(s);
|
||||
}).then(()=>{
|
||||
document.querySelectorAll('.math-pending').forEach(el=>{
|
||||
try{ katex.render(el.textContent, el, {...MATH_OPTS, displayMode: el.dataset.d === '1'}); }catch(_){}
|
||||
el.classList.remove('math-pending');
|
||||
});
|
||||
}).catch(()=>{ mathLoading = null; });
|
||||
return mathLoading;
|
||||
}
|
||||
@@ -14,7 +14,13 @@ function fixMd(s){
|
||||
s = s.replace(/\n{3,}/g, '\n\n');
|
||||
return s;
|
||||
}
|
||||
function md(src){ return src ? marked.parse(fixMd(src)) : ''; }
|
||||
// Les formules LaTeX sont mises de côté avant marked puis rendues par KaTeX
|
||||
// (00-math.js) ; le code et les montants en $ ne sont pas touchés.
|
||||
function md(src){
|
||||
if(!src) return '';
|
||||
const m = mathExtract(fixMd(src));
|
||||
return mathRestore(marked.parse(m.text), m.list, mathHtml);
|
||||
}
|
||||
// Les garde-fous du serveur ("[stop: trop d'appels d'outils]") sont concaténés
|
||||
// au texte de la réponse : ils arrivent donc en clair, au milieu du markdown.
|
||||
// markNotices les sort du fil pour qu'on ne les prenne pas pour une phrase du
|
||||
|
||||
@@ -1383,6 +1383,10 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
||||
border:1px solid var(--border);border-radius:999px;background:var(--panel);
|
||||
color:var(--dim);font-size:12px;line-height:1.5}
|
||||
.stopnote::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dim)}
|
||||
/* Formules LaTeX (KaTeX, 00-math.js) : une longue formule défile au lieu de déborder. */
|
||||
.katex{font-size:1.08em}
|
||||
.katex-display{margin:.6em 0;overflow-x:auto;overflow-y:hidden;padding:2px 0}
|
||||
.math-pending{font-family:var(--mono);font-size:.92em;color:var(--dim)}
|
||||
|
||||
/* ===== Fenêtres et sélecteurs =====================================
|
||||
Coins arrondis et ombre douce plutôt qu'un cadre marqué. */
|
||||
|
||||
Reference in new issue
Block a user