import { j3pAddElt, j3pElement, j3pEnsureHtmlElement, j3pGetCssProp, j3pGetNewId } from 'src/legacy/core/functions'
import { MathfieldElement, renderMathInElement } from 'mathlive'
import { convertRestriction } from 'src/lib/utils/regexp'
import VirtualKeyboard from 'src/lib/widgets/mlVirtualKeyboard/VirtualKeyboard'
import { inputAutoSizeListener } from 'src/lib/mathquill/functions'
import { cleanLatexForMl, correctArrayLignes, replaceOldMQCodes, traiteEspacementLignesMatrices, transposeMatrices } from 'src/lib/outils/mathlive/utils'
import { getIndexFermant } from 'src/lib/utils/string'
// Fonction je l’espère provisoire remplaçant les codes \le et \ge mis automatiquement par Mathlive
// à la place des ≤ et ≥ dans les \text{} (de dernier niveau)
function correctInegCodes (ch) {
const st = ch
let indstart = 0
while ((indstart = ch.indexOf('\\text{', indstart)) !== -1) {
const indfin = getIndexFermant(ch, indstart + 5)
if (indfin === -1) return st
const content = ch.substring(indstart + 6, indfin)
const newcontent = content.replace(/≤/g, '$\\le$').replace(/≥/g, '$\\ge$')
ch = ch.substring(0, indstart + 6) + newcontent + ch.substring(indfin)
indstart = indfin + newcontent.length - content.length + 1
}
return ch
}
/**
* Fonction affichant la chaîne ch en la transformant par un contenu compatible mathlive
* @param {HTMLElement} container le conteneur
* @param {string} idspan l’id à affecter au span qui va être créé et contiendra l’affichage MathQuill
* @param {string} ch la chaîne à traiter
* @param {Object} [options]
* @param {string} [options.charset]
* @param {boolean} [options.charsetText]
* @param {string[]} [options.listeBoutons]
* @param {boolean} [options.transposeMat] mettre à true si on peut que les matrices soient transposées
* avant le remplacement des \editable{} pas des placeholders pour compatibilité avec les anciennes sections
* puis retransposées après pour être compatible avec l’ancienne version MathQuill (inversions des lignes et des colonnes)
* @param {boolean} [options.replacefracdfrac] Si présent et true, on remplace les codes \frac par des codes \dfrac
* @param {boolean} [options.cleanHtml] si present et false, on ne nettoie pas le code HTML
*/
export function afficheMathliveDans (container, idspan, ch, options = {}) {
if (typeof ch === 'number') {
if (!Number.isFinite(ch)) {
throw Error(`Il faut passer une chaîne (number ${ch})`)
}
// sinon on râle mais on tolère
console.warn(Error(`afficheMathliveDans veut une string (number ${ch})`))
ch = String(ch)
}
if (typeof ch !== 'string') {
throw Error(`Chaîne invalide (${typeof ch} ${ch})`)
}
// on dit rien sur les chaînes vides
if (!ch) return
MathfieldElement.plonkSound = null // On désactive tous les sons en global
if (idspan && typeof idspan === 'string') {
// on s’assure qu’il n’y a pas déjà un élément avec cet id (et si jamais y’en avait un ça va râler en console et lui ajouter un suffixe numérique)
idspan = j3pGetNewId(idspan, true)
} else {
// sinon on en génère un car il sert ensuite de préfixe un peu partout
idspan = j3pGetNewId('affiche')
}
const charset = options.charset || ''
const charsetText = options.charsetText || ''
const listeBoutons = options.listeBoutons || []
container = j3pEnsureHtmlElement(container)
const realcont = j3pEnsureHtmlElement(container)
// On corrige une icncompatibilité d’affichage entre Mathjax et Mathlive pour les array d’une seule ligne
ch = correctArrayLignes(ch)
// Important ! : Ligne suivante : vec MathLive on se retrouve avec de \right) affichés quand des \right[ contienney
// des \left( avec du \displaystyle dedans
// Semble résolu avec la version 0.98.5 de Mathlive
// ch = ch.replace(/\\displaystyle/g, ' ')
const span = j3pAddElt(realcont, 'span', '', { id: idspan })
// On remplace les éléments passés en paramètre via & suivi d’une lettre
if (options) {
for (const [key, value] of Object.entries(options)) {
if (key.length === 1) ch = ch.replace('&' + key, value)
}
}
// On remplace les éventuels paramètres
/*
Array.from('abcdefghijklmnopqrstuvwxyz').forEach((value) => {
// Attention : Mathlive n’aime pas les \n qui pourraient se trouver dans les paramètres Latex
if (options[value]) ch = ch.replace(new RegExp('£' + value, 'g'), options[value].replaceAll('\n', ' '))
})
*/
for (let [prop, value] of Object.entries(options)) {
// avec ou sans {}, indépendamment de la longueur de p (une lettre ou plusieurs)
if (typeof value === 'number') value = String(value)
if (typeof value === 'string') {
value = value.replaceAll('\n', ' ')
ch = ch
.replace(new RegExp('£{' + prop + '}', 'g'), value)
.replace(new RegExp('£' + prop, 'g'), value)
}
}
// On nettoie ch des balises interdites
if (options.cleanHtml !== false) ch = resumeHtml(escHtml(ch))
ch = replaceOldMQCodes(ch)
// Ligne suivante à supprimer une fois Mathlive corrigé
ch = correctInegCodes(ch)
// On crée les inputs
if (ch.includes('@1@')) {
let indText = 1
while (ch.includes('@' + indText + '@')) {
const idtext = '@' + indText + '@'
const inddeb = ch.indexOf(idtext)
const indfin = inddeb + idtext.length
const inputValue = options['input' + indText]
const value = inputValue.texte === '' ? '' : 'value="' + inputValue.texte + '"'
const color = inputValue.couleur || options?.style?.color
const fontSize = (inputValue?.taillepolice && (inputValue.taillepolice + 'px')) || j3pGetCssProp(container, 'fontSize') || '20px'
const size = inputValue.taille || 20
const largDynDefaut = 20
const largInputDefaut = 100
const width = 'width:' + (inputValue.dynamique ? largDynDefaut : largInputDefaut) + 'px'
let maxchars
if (typeof inputValue.maxchars === 'number' || (typeof inputValue.maxchars === 'string' && /^\d+$/.test(inputValue.maxchars))) {
maxchars = inputValue.maxchars
} else {
maxchars = ''
}
let maxlength
if (typeof inputValue.maxlength === 'number' || (typeof inputValue.maxlength === 'string' && /^\d+$/.test(inputValue.maxlength))) {
maxlength = inputValue.maxlength
} else {
maxlength = maxchars // éventuellement vide
}
let style = `size:${size};${width};font-size:${fontSize};`
if (color) style += `color:${color};`
const textId = j3pGetNewId(idspan + 'input' + indText, true)
// Si on a un charsetText, on donne à l’input la classe MLInput ce qui permettra du lui affecter un clavier virtuel
const cl = charsetText ? 'class = "inputWithKeyboard"' : ''
const html = `<span><input type="text" inputmode="none" maxchars="${maxchars}" autocomplete="off" size="${size}" id="${textId}" maxlength="${maxlength}" ${cl} ${value} style="${style}"></span> `
ch = ch.substring(0, inddeb) + html + ch.substring(indfin)
indText++
}
}
// On crée les listes déroulantes
if (ch.includes('#1#')) {
let indListe = 1
while (ch.includes('#' + indListe + '#')) {
const idliste = '#' + indListe + '#'
const inddeb = ch.indexOf(idliste)
const indfin = inddeb + idliste.length
const opt = options['liste' + indListe]
const fontSize = (opt?.taillepolice && (opt.taillepolice + 'px')) || j3pGetCssProp(container, 'fontSize') || '18px'
let style = `font-size:${fontSize};`
const color = opt?.couleur || j3pGetCssProp(container, 'color')
if (color) style += `color:${color};`
const spanId = j3pGetNewId(idspan + 'spanListe' + indListe, true)
const selectId = j3pGetNewId(idspan + 'liste' + indListe, true)
let html = `<span id="${spanId}"><form style="display:inline"><select id="${selectId}" size="1" style="${style}">`
if (opt?.texte) {
for (const txt of opt.texte) {
html += '<option>' + txt + '</option>'
}
} else {
console.error(Error('Aucune option texte à mettre dans le select'))
html += '<option>Désolé, aucun choix n’a été paramétré</option>'
}
html += '</select></form></span>'
ch = ch.substring(0, inddeb) + html + ch.substring(indfin)
indListe++
}
// S’il reste encore des list suivi d’un chiffre c’est une erreur (des indices de list non consécutifs)
if (/#\d+/g.test(ch)) {
console.error(Error('indices de liste non consécutifs dans afficheMathlive'))
}
}
// On crée les éditeurs de formule simples
let i = 1
while (ch.includes('&' + i + '&')) {
ch = ch.replace('&' + i + '&', '<math-field id=' + idspan + 'inputmq' + i + '></math-field>')
i++
}
// On crée les éditeurs de formule pour les editable. Un seul éditeur mathfield pour le contenu des $
// Les éditeurs mathlive contenant des placeHolder ont pour classe PhBlock et une id
// qui est expressionPhBlock suivi de l’indice de cet éditeur dans expression
// Les placeHolder qu’il contient on pour id l’id de ce bloc suivi de ph suivi de leur
// indice global dans l’expression
// A cause du traitement différent des matrices dans Mathquill et MathLive, on transpose
// d’abord les matrices de ch puis on affecte les numéros des \editable qui sont ensuite remplacés
// par des placeholder puis on transpose à nouveau toutes les matrices du résultat
const hasEditable = options.transposeMat && ch.includes('\\begin{matrix}') && ch.includes('\\editable{}')
if (hasEditable) ch = transposeMatrices(ch)
let nbEditable = 1 // Nombre global des editable rencontrés
// Chacun des blocs contenant des editable aura pour id PhBlock (Ph pour placeholder) suivi de son numéro
let nbPhBlocks = 0
while (ch.includes('\\editable{}')) {
nbPhBlocks++
// On recherche le premier caractère $ qui précède
const indedit = ch.indexOf('\\editable{}')
let j
for (j = indedit - 1; j >= 0; j--) {
if (ch.charAt(j) === '$') break
}
let k = ch.indexOf('$', indedit)
if (k === -1) k = ch.length
let str = ch.substring(j + 1, k)
const id = idspan + 'PhBlock' + nbPhBlocks
while (str.includes('\\editable{}')) {
str = str.replace('\\editable{}', '\\placeholder[' + id + 'ph' + nbEditable + ']{#0}')
nbEditable++
}
ch = ch.substring(0, j) + '<math-field readonly class= "PhBlock" ' + 'id=' + id + '>' + str + '</math-field>' + ch.substring(k + 1)
}
// On retranspose pour retrouver les matrices de départ si on a déjà transposé
if (hasEditable) ch = transposeMatrices(ch)
// le replacer du contenu entre $
const replacer = (match, p1) => {
const content = p1
// ajoute une espace après chaque < immédiatement suivi d'une lettre (bug mathlive)
.replace(/<([a-zA-Z])/g, '< $1')
// Il ne faut pas de \displaystyle à l’intérieur des matrices Mathlive n’aime pas
.replaceAll('\\displaystyle', ' ')
const newcontent = traiteEspacementLignesMatrices(content)
// Et on met les formules en inline
return '$' + '\\displaystyle ' + newcontent + '$'
}
if (options.replacefracdfrac) ch = ch.replace(/\\frac/g, '\\dfrac')
ch = ch.replace(/\$([^$]+)\$/g, replacer)
const st = ch.split('<br>')
for (let i = 0; i < st.length; i++) {
// La première ligne est inline et les suivantes dans des <p>
const par = j3pAddElt(span, i === 0 ? 'span' : 'p', st[i])
par.style.paddingTop = '4px' // Pour que les lignes ne see chevauchent pas
par.style.paddingBottom = '4px' // Pour que les lignes ne see chevauchent pas
if (options?.style) {
for (const [key, value] of Object.entries(options.style)) {
par.style[key] = value
}
}
// renderMathInElement(par, { renderAccessibleContent: '' })
renderMathInElement(par, {
renderAccessibleContent: '', // Pas de message pour non-voyants etc ...
TeX: {
delimiters: {
// Allow math formulas surrounded by $...$ to be rendered as inline (textstyle) content.
inline: [
['$', '$']
],
display: [] // Pas de délimiteurs pour passer en mode texte
}
}
})
}
let restriction = charset
if (typeof restriction === 'string') restriction = convertRestriction(restriction)
let restrictionInput = charsetText
if (typeof restrictionInput === 'string') restrictionInput = convertRestriction(restrictionInput)
container.querySelectorAll('math-field').forEach((mf) => {
mf.menuItems = []
mf.inlineShortcuts = { // Pas de shortcuts à part ces deux là
'*': restriction.test('*') ? '\\times' : '', // On affecte à la touche * le signe de multiplication (\cdot par défaut)
'.': restriction.test('.') ? ',' : '',
'[': '[', // Ne pas tester car si '', insère des double crochets automatiquement
'<': '<',
$: '' // Pas de caractère $
}
// eslint-disable-next-line no-new
new VirtualKeyboard(mf, restriction, {
commandes: listeBoutons
})
// On déplace toute cette logique dans le constructeur de VirtualKeyboard
// addEventListenersToEditor(mf)
// Je retire le bouton de menu à la main plutôt qu’avec le css pas très fiable
// const menuToggleButton = mf.shadowRoot.querySelector('.ML__menu-toggle')
// menuToggleButton.parentNode.removeChild(menuToggleButton)
// const toggleButton = mf.shadowRoot.querySelector('.ML__virtual-keyboard-toggle')
// ON va remplacer le "bouton' mathlive par notre propre bouton
// const divtb = createToggleButton(mf)
// toggleButton.parentNode.replaceChild(divtb, toggleButton)
// On laisse de la place entre le champ d’édition et le bouton permettant de basculer le clavier
// mf.shadowRoot.querySelector('.ML__content').style.marginRight = '8px'
})
container.querySelectorAll('input[type=text]').forEach((input) => {
if (input.classList.contains('inputWithKeyboard')) { // Si l’input texte a un clavier virtuel associé
// eslint-disable-next-line no-new
new VirtualKeyboard(input, restrictionInput, {
acceptSpecialChars: true // Si le caractère espace est dans le charSet pour un input texte on le met dans le clavier
})
// On déplace toute cette logique dans le constructeur de VirtualKeyboard
// addEventListenersToEditor(input)
// const divtb = createToggleButton(input)
// input.parentNode.appendChild(divtb)
}
// Il faut rendre tous les input de texte autosize
input.addEventListener('input', inputAutoSizeListener, false)
})
} // afficheMathliveDans
// faut remplacer temporairement les et autres < pour que le & ne soit pas interprété comme mqedit
// mais aussi les tags html, pour les contrôler et autoriser le < dans la string
// Bonne idée ce choix de & comme caractère spécial :-D
function escHtml (ch) {
if (!ch) return ''
return ch
// lui n’a pas besoin d’un tag car le \n est géré
.replace(/<br *\/?>/gi, '\n')
// il y a des tonnes de sections qui envoient du html, qu’il faut conserver… on liste qq tags autorisés
// tag auto fermant
.replace(/<hr(\s+\/)?>/gi, '¿¿¿¿hr¡¡¡¡')
// tags sans attributes (*? pour être "non gourmand" => s’arrêter au premier </tagFermant> trouvé et pas au dernier)
.replace(/<(b|em|i|strong|sub|sup|u)>(.*?)<\/\1>/gsi, (match, tag, content) => `¿¿¿¿${tag}¡¡¡¡${content}¿¿¿¿/${tag}¡¡¡¡`)
// on autorise img|p|span avec attributs, mais pas n’importe lesquels (surtout pas onload par ex, ça permettrait d’exécuter du js arbitraire)
.replace(/<img( [^>]+)\/?>/gs, (match, strAttrs) => {
if (strAttrs) return `¿¿¿¿img ${cleanAttributes(strAttrs)}/¡¡¡¡`
console.error(Error('tag <img /> sans attributs'))
return ''
})
.replace(/<(p|span)( [^>]+)?>(.*?)<\/\1>/gs, (match, tag, strAttrs, content) => {
if (strAttrs) return `¿¿¿¿${tag} ${cleanAttributes(strAttrs)}¡¡¡¡${content}¿¿¿¿/${tag}¡¡¡¡`
return `¿¿¿¿${tag}¡¡¡¡${content}¿¿¿¿/${tag}¡¡¡¡`
})
.replace(/&(nbsp|lt|gt|dollar);/g, (match, chunk) => '¿¿¿' + chunk + '¡¡¡')
.replace(/&#([0-9]+);/g, (match, chunk) => '¿¿¡' + chunk + '¡¡¡')
}
function resumeHtml (ch) {
if (!ch) return ''
return ch
.replace(/¿¿¿¿/g, '<')
.replace(/¡¡¡¡/g, '>')
.replace(/¿¿¿/g, '&')
.replace(/¿¿¡/g, '&#')
.replace(/¡¡¡/g, ';')
.replace(/\n/g, '<br>') // on remplace les éventuels \n mis dans contenu au départ
}
/**
* Contrôle les attributs qu’on autorise (class|width|height) et supprime les autres (faut pas de style ni de onload="js arbitraire")
* @private
* @param {string} strAttrs liste d’attributs x="y"
* @returns {string} les attributs autorisés
*/
function cleanAttributes (strAttrs) {
if (!strAttrs) return ''
let cleanAttrs = ''
let reste = strAttrs // faut pas modifier la string dans le while
const regex = /(alt|class|height|src|tabindex|width)=(['"])(.*?)\2/g
let chunks
while ((chunks = regex.exec(strAttrs)) != null) {
const [match] = chunks
cleanAttrs += ` ${match}`
reste = reste.replace(match, '')
}
reste = reste.replace(/\s/g, '')
if (reste) {
if (!/^\s*\/\s*$/.test(reste)) console.warn(Error(`attributs ignorés : ${reste}`))
}
return cleanAttrs
}
/**
* Retourne la valeur d’un input mathlive (en latex)
* @param {HTMLElement|string} elt
* @param {Object} [options]
* @param {string} [options.placeholderId] si fourni on fera du getPromptValue de ce placeHolder (plutôt que du getValue sur elt)
* @param {string} [options.raw = false] passer true pour retourner la valeur brute du champ (sans la passer à cleanLatexForMl avant de la retourner)
* @return {string}
*/
export function getMathliveValue (elt, { placeholderId, raw } = {}) {
if (typeof elt === 'string') elt = j3pElement(elt)
if (!elt || elt.tagName.toUpperCase() !== 'MATH-FIELD') {
console.error(Error('élément mathlive invalide'), elt)
// important de retourner une string pour éviter que la suite plante
return ''
}
const value = placeholderId
? elt.getPromptValue(placeholderId, 'latex-unstyled')
: elt.getValue('latex-unstyled')
if (raw) return value ?? ''
return cleanLatexForMl(value)
}