All checks were successful
Deploy MinIO Explorer / deploy (push) Successful in 34s
870 lines
28 KiB
JavaScript
870 lines
28 KiB
JavaScript
// Configuration globale
|
|
let config = {
|
|
baseUrl: '',
|
|
currentPath: ''
|
|
};
|
|
|
|
// Initialisation de l'application
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeApp();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function initializeApp() {
|
|
// Tentative de configuration automatique depuis l'URL courante
|
|
autoConfigureFromURL();
|
|
|
|
// Affichage du formulaire de développement
|
|
showDevForm();
|
|
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Formulaire de développement
|
|
const urlForm = document.getElementById('url-form');
|
|
if (urlForm) {
|
|
urlForm.addEventListener('submit', handleUrlFormSubmit);
|
|
}
|
|
|
|
// Gestion de la navigation arrière/avant du navigateur
|
|
window.addEventListener('popstate', handlePopState);
|
|
|
|
// Gestionnaires pour la prévisualisation
|
|
const closePreview = document.getElementById('close-preview');
|
|
if (closePreview) {
|
|
closePreview.addEventListener('click', hidePreview);
|
|
}
|
|
|
|
const downloadFile = document.getElementById('download-file');
|
|
if (downloadFile) {
|
|
downloadFile.addEventListener('click', downloadCurrentFile);
|
|
}
|
|
}
|
|
|
|
function handlePopState(event) {
|
|
console.log('Navigation arrière/avant détectée:', event.state);
|
|
|
|
if (event.state && event.state.baseUrl) {
|
|
// Restaurer la configuration
|
|
config.baseUrl = event.state.baseUrl;
|
|
config.currentPath = event.state.path || '';
|
|
|
|
updateConfigDisplay();
|
|
|
|
// Charger le contenu sans ajouter à l'historique (pour éviter les boucles)
|
|
loadBucketContents(config.currentPath, false);
|
|
}
|
|
}
|
|
|
|
function autoConfigureFromURL() {
|
|
const currentUrl = window.location.href;
|
|
const url = new URL(currentUrl);
|
|
|
|
// Configuration simplifiée : on prend juste le base URL sans le bucket
|
|
config.baseUrl = `${url.protocol}//${url.host}${url.pathname.replace('/index.html', '')}`;
|
|
|
|
// Vérifier s'il y a un fragment d'URL pour définir le chemin initial
|
|
if (url.hash) {
|
|
const hashPath = url.hash.replace('#/', '');
|
|
config.currentPath = hashPath;
|
|
}
|
|
|
|
updateConfigDisplay();
|
|
console.log('Configuration automatique:', config);
|
|
|
|
// Charger automatiquement le contenu si on a une config valide
|
|
if (config.baseUrl) {
|
|
loadBucketContents(config.currentPath, false);
|
|
}
|
|
}
|
|
|
|
function handleUrlFormSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
const urlInput = document.getElementById('minio-url');
|
|
if (!urlInput) return;
|
|
|
|
const minioUrl = urlInput.value.trim();
|
|
|
|
if (!minioUrl) {
|
|
showError('Veuillez saisir une URL MinIO');
|
|
return;
|
|
}
|
|
|
|
parseMinioURL(minioUrl);
|
|
}
|
|
|
|
function parseMinioURL(url) {
|
|
try {
|
|
const urlObj = new URL(url);
|
|
|
|
// Configuration simplifiée : on prend l'URL complète comme base
|
|
config.baseUrl = url.replace(/\/$/, ''); // Supprimer le slash final si présent
|
|
config.currentPath = '';
|
|
|
|
updateConfigDisplay();
|
|
|
|
// Ajouter l'état initial à l'historique
|
|
const initialState = { path: '', baseUrl: config.baseUrl };
|
|
history.replaceState(initialState, '', window.location.href);
|
|
|
|
loadBucketContents();
|
|
|
|
} catch (error) {
|
|
showError('URL invalide: ' + error.message);
|
|
console.error('Erreur de parsing URL:', error);
|
|
}
|
|
}
|
|
|
|
function updateConfigDisplay() {
|
|
const configInfo = document.getElementById('config-info');
|
|
if (configInfo) {
|
|
configInfo.innerHTML = `
|
|
<strong>Configuration actuelle:</strong><br>
|
|
URL de base: ${config.baseUrl}<br>
|
|
Chemin actuel: /${config.currentPath}
|
|
`;
|
|
}
|
|
}
|
|
|
|
function showDevForm() {
|
|
const devForm = document.getElementById('dev-form');
|
|
if (devForm) {
|
|
devForm.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
|
|
function showError(message) {
|
|
const errorSection = document.getElementById('error-section');
|
|
const errorMessage = document.getElementById('error-message');
|
|
|
|
if (errorMessage) {
|
|
errorMessage.textContent = message;
|
|
}
|
|
|
|
if (errorSection) {
|
|
errorSection.style.display = 'block';
|
|
|
|
// Masquer l'erreur après 5 secondes
|
|
setTimeout(() => {
|
|
errorSection.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
function showLoading(show = true) {
|
|
const loading = document.getElementById('loading');
|
|
const table = document.getElementById('files-table');
|
|
|
|
if (loading) {
|
|
loading.style.display = show ? 'block' : 'none';
|
|
}
|
|
|
|
if (table) {
|
|
table.style.display = show ? 'none' : 'table';
|
|
}
|
|
}
|
|
|
|
function updateCurrentPathDisplay() {
|
|
const pathDisplay = document.getElementById('current-path');
|
|
if (pathDisplay) {
|
|
pathDisplay.textContent = '/' + config.currentPath;
|
|
}
|
|
}
|
|
|
|
async function loadBucketContents(path = '', pushToHistory = true) {
|
|
console.log('Debug loadBucketContents - path received:', JSON.stringify(path));
|
|
|
|
if (!config.baseUrl) {
|
|
showError('Configuration URL manquante');
|
|
return;
|
|
}
|
|
|
|
showLoading(true);
|
|
config.currentPath = path;
|
|
updateCurrentPathDisplay();
|
|
|
|
// Ajouter à l'historique du navigateur si demandé
|
|
if (pushToHistory) {
|
|
const state = { path: path, baseUrl: config.baseUrl };
|
|
const url = path ? `#/${path}` : '#/';
|
|
history.pushState(state, '', url);
|
|
}
|
|
|
|
try {
|
|
// Construction de l'URL API avec delimiter
|
|
let apiUrl = `${config.baseUrl}?list-type=2&delimiter=/`;
|
|
|
|
// Construire le prefix pour l'API MinIO
|
|
if (path) {
|
|
// Pour un sous-dossier
|
|
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
|
const fullPrefix = cleanPath + '/';
|
|
apiUrl += `&prefix=${encodeURIComponent(fullPrefix)}`;
|
|
}
|
|
|
|
console.log('Appel API MinIO:', apiUrl);
|
|
|
|
const response = await fetch(apiUrl);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
|
|
const xmlText = await response.text();
|
|
console.log('Réponse XML reçue:', xmlText);
|
|
|
|
const contents = parseMinioXML(xmlText);
|
|
displayContents(contents);
|
|
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement:', error);
|
|
showError('Erreur de chargement: ' + error.message);
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
function parseMinioXML(xmlText) {
|
|
const parser = new DOMParser();
|
|
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
|
|
|
const contents = {
|
|
folders: [],
|
|
files: []
|
|
};
|
|
|
|
// Déterminer le préfixe attendu basé sur la navigation actuelle
|
|
let expectedPrefix = '';
|
|
if (config.currentPath) {
|
|
expectedPrefix = config.currentPath + '/';
|
|
}
|
|
|
|
// Parse CommonPrefixes (dossiers)
|
|
const prefixes = xmlDoc.getElementsByTagName('CommonPrefixes');
|
|
for (let prefix of prefixes) {
|
|
const prefixElement = prefix.getElementsByTagName('Prefix')[0];
|
|
if (prefixElement) {
|
|
const fullPrefix = prefixElement.textContent;
|
|
|
|
// Extraire le nom du dossier (dernière partie avant le /)
|
|
const folderName = fullPrefix.replace(/\/$/, '').split('/').pop();
|
|
|
|
if (folderName) {
|
|
// Construire le chemin pour la navigation
|
|
let folderPath;
|
|
if (config.currentPath) {
|
|
folderPath = config.currentPath + '/' + folderName;
|
|
} else {
|
|
folderPath = folderName;
|
|
}
|
|
|
|
console.log('Debug parsing - fullPrefix:', fullPrefix, 'folderName:', folderName, 'currentPath:', config.currentPath, 'folderPath:', folderPath);
|
|
|
|
contents.folders.push({
|
|
name: folderName,
|
|
path: folderPath
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse Contents (fichiers)
|
|
const objects = xmlDoc.getElementsByTagName('Contents');
|
|
for (let obj of objects) {
|
|
const keyElement = obj.getElementsByTagName('Key')[0];
|
|
const sizeElement = obj.getElementsByTagName('Size')[0];
|
|
const modifiedElement = obj.getElementsByTagName('LastModified')[0];
|
|
|
|
if (keyElement) {
|
|
const fullKey = keyElement.textContent;
|
|
const fileName = fullKey.split('/').pop();
|
|
|
|
// Vérifier que le fichier correspond au niveau de navigation actuel
|
|
let shouldInclude = false;
|
|
|
|
if (config.currentPath) {
|
|
// Dans un sous-dossier : vérifier que la clé commence par le bon préfixe
|
|
const currentPrefix = config.currentPath + '/';
|
|
if (fullKey.startsWith(currentPrefix)) {
|
|
// S'assurer qu'on n'affiche que les fichiers directs (pas dans des sous-sous-dossiers)
|
|
const relativePath = fullKey.replace(currentPrefix, '');
|
|
if (!relativePath.includes('/')) {
|
|
shouldInclude = true;
|
|
}
|
|
}
|
|
} else {
|
|
// À la racine : afficher tous les fichiers qui ne sont pas dans des dossiers
|
|
if (!fullKey.includes('/')) {
|
|
shouldInclude = true;
|
|
}
|
|
}
|
|
|
|
// Exclure les fichiers de l'application
|
|
if (shouldInclude && fileName && !['index.html', 'style.css', 'script.js'].includes(fileName)) {
|
|
contents.files.push({
|
|
name: fileName,
|
|
size: sizeElement ? parseInt(sizeElement.textContent) : 0,
|
|
modified: modifiedElement ? new Date(modifiedElement.textContent) : new Date(),
|
|
fullKey: fullKey
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return contents;
|
|
}
|
|
|
|
function displayContents(contents) {
|
|
const tbody = document.getElementById('files-tbody');
|
|
if (!tbody) return;
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
// Bouton parent directory si on n'est pas à la racine
|
|
if (config.currentPath) {
|
|
const parentPath = config.currentPath.split('/').slice(0, -1).join('/');
|
|
const parentRow = createParentRow(parentPath);
|
|
tbody.appendChild(parentRow);
|
|
}
|
|
|
|
// Afficher les dossiers
|
|
contents.folders.forEach(folder => {
|
|
const row = createFolderRow(folder);
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Afficher les fichiers
|
|
contents.files.forEach(file => {
|
|
const row = createFileRow(file);
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Afficher le tableau
|
|
const filesTable = document.getElementById('files-table');
|
|
if (filesTable) {
|
|
filesTable.style.display = 'table';
|
|
}
|
|
|
|
// Chercher et ouvrir automatiquement index.rst s'il existe
|
|
autoOpenIndexRst(contents.files);
|
|
}
|
|
|
|
function createParentRow(parentPath) {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>
|
|
<span class="icon-parent"></span>
|
|
<a href="#" class="folder-link parent-dir" data-path="${parentPath}">..</a>
|
|
</td>
|
|
<td>-</td>
|
|
<td>-</td>
|
|
<td>-</td>
|
|
`;
|
|
|
|
const link = row.querySelector('.folder-link');
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
loadBucketContents(parentPath, true);
|
|
});
|
|
|
|
return row;
|
|
}
|
|
|
|
function createFolderRow(folder) {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>
|
|
<span class="icon-folder"></span>
|
|
<a href="#" class="folder-link" data-path="${folder.path}">${folder.name}/</a>
|
|
</td>
|
|
<td>-</td>
|
|
<td>-</td>
|
|
<td>-</td>
|
|
`;
|
|
|
|
const link = row.querySelector('.folder-link');
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
loadBucketContents(folder.path, true);
|
|
});
|
|
|
|
return row;
|
|
}
|
|
|
|
function createFileRow(file) {
|
|
const row = document.createElement('tr');
|
|
const fileUrl = `${config.baseUrl}/${file.fullKey}`;
|
|
|
|
row.innerHTML = `
|
|
<td>
|
|
<span class="icon-file"></span>
|
|
<a href="#" class="file-link" data-file-url="${fileUrl}" data-file-name="${file.name}">${file.name}</a>
|
|
</td>
|
|
<td class="file-size">${formatFileSize(file.size)}</td>
|
|
<td class="file-date">${formatDate(file.modified)}</td>
|
|
<td>
|
|
<button class="copy-btn" data-url="${fileUrl}">Copier lien</button>
|
|
</td>
|
|
`;
|
|
|
|
const fileLink = row.querySelector('.file-link');
|
|
fileLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
showPreview(fileUrl, file.name);
|
|
});
|
|
|
|
const copyBtn = row.querySelector('.copy-btn');
|
|
copyBtn.addEventListener('click', () => copyToClipboard(fileUrl));
|
|
|
|
return row;
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatDate(date) {
|
|
return date.toLocaleString('fr-FR', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
async function copyToClipboard(text) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
console.log('Lien copié:', text);
|
|
|
|
// Feedback visuel temporaire
|
|
const btn = event.target;
|
|
const originalText = btn.textContent;
|
|
btn.textContent = 'Copié !';
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
}, 1000);
|
|
|
|
} catch (error) {
|
|
console.error('Erreur copie clipboard:', error);
|
|
|
|
// Fallback pour les navigateurs plus anciens
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
document.body.appendChild(textArea);
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
}
|
|
}
|
|
|
|
// Variables globales pour la prévisualisation
|
|
let currentPreviewFile = {
|
|
url: '',
|
|
name: ''
|
|
};
|
|
|
|
function detectFileType(fileName) {
|
|
const extension = fileName.toLowerCase().split('.').pop();
|
|
|
|
const fileTypes = {
|
|
images: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'],
|
|
text: ['txt', 'json', 'xml', 'csv', 'log'],
|
|
code: ['js', 'css', 'html', 'htm', 'py', 'java', 'cpp', 'c', 'php', 'rb', 'go', 'rs', 'ts', 'jsx', 'tsx', 'vue', 'svelte', 'tex', 'latex'],
|
|
markdown: ['md', 'markdown'],
|
|
rst: ['rst', 'rest'],
|
|
pdf: ['pdf'],
|
|
video: ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'],
|
|
audio: ['mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a'],
|
|
archive: ['zip', 'rar', '7z', 'tar', 'gz', 'bz2']
|
|
};
|
|
|
|
for (const [type, extensions] of Object.entries(fileTypes)) {
|
|
if (extensions.includes(extension)) {
|
|
return type;
|
|
}
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
function showPreview(fileUrl, fileName) {
|
|
currentPreviewFile = { url: fileUrl, name: fileName };
|
|
|
|
const previewSection = document.getElementById('preview-section');
|
|
const previewTitle = document.getElementById('preview-title');
|
|
const previewContent = document.getElementById('preview-content');
|
|
|
|
if (!previewSection || !previewTitle || !previewContent) {
|
|
console.error('Éléments de prévisualisation non trouvés');
|
|
return;
|
|
}
|
|
|
|
previewTitle.textContent = `Prévisualisation: ${fileName}`;
|
|
previewContent.innerHTML = '<div style="text-align: center; padding: 20px;">Chargement...</div>';
|
|
|
|
previewSection.style.display = 'flex';
|
|
|
|
const fileType = detectFileType(fileName);
|
|
|
|
switch (fileType) {
|
|
case 'images':
|
|
showImagePreview(fileUrl, previewContent);
|
|
break;
|
|
case 'text':
|
|
case 'code':
|
|
showTextPreview(fileUrl, previewContent);
|
|
break;
|
|
case 'markdown':
|
|
showMarkdownPreview(fileUrl, previewContent);
|
|
break;
|
|
case 'rst':
|
|
showRstPreview(fileUrl, previewContent);
|
|
break;
|
|
case 'pdf':
|
|
showPdfPreview(fileUrl, previewContent);
|
|
break;
|
|
case 'video':
|
|
showVideoPreview(fileUrl, previewContent);
|
|
break;
|
|
case 'audio':
|
|
showAudioPreview(fileUrl, previewContent);
|
|
break;
|
|
default:
|
|
showUnsupportedPreview(fileName, previewContent);
|
|
}
|
|
}
|
|
|
|
function showImagePreview(fileUrl, container) {
|
|
const img = document.createElement('img');
|
|
img.src = fileUrl;
|
|
img.alt = 'Prévisualisation image';
|
|
|
|
img.onload = () => {
|
|
container.innerHTML = '';
|
|
container.appendChild(img);
|
|
};
|
|
|
|
img.onerror = () => {
|
|
container.innerHTML = '<div class="preview-error">Erreur: Impossible de charger l\'image</div>';
|
|
};
|
|
}
|
|
|
|
function showTextPreview(fileUrl, container) {
|
|
fetch(fileUrl)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
})
|
|
.then(text => {
|
|
const pre = document.createElement('pre');
|
|
pre.textContent = text;
|
|
container.innerHTML = '';
|
|
container.appendChild(pre);
|
|
})
|
|
.catch(error => {
|
|
console.error('Erreur chargement texte:', error);
|
|
container.innerHTML = '<div class="preview-error">Erreur: Impossible de charger le fichier texte</div>';
|
|
});
|
|
}
|
|
|
|
function showPdfPreview(fileUrl, container) {
|
|
const iframe = document.createElement('iframe');
|
|
iframe.src = fileUrl;
|
|
iframe.style.width = '100%';
|
|
iframe.style.height = '500px';
|
|
|
|
container.innerHTML = '';
|
|
container.appendChild(iframe);
|
|
}
|
|
|
|
function showVideoPreview(fileUrl, container) {
|
|
const video = document.createElement('video');
|
|
video.src = fileUrl;
|
|
video.controls = true;
|
|
video.style.maxWidth = '100%';
|
|
|
|
container.innerHTML = '';
|
|
container.appendChild(video);
|
|
}
|
|
|
|
function showAudioPreview(fileUrl, container) {
|
|
const audio = document.createElement('audio');
|
|
audio.src = fileUrl;
|
|
audio.controls = true;
|
|
audio.style.width = '100%';
|
|
|
|
container.innerHTML = '';
|
|
container.appendChild(audio);
|
|
}
|
|
|
|
function showUnsupportedPreview(fileName, container) {
|
|
const extension = fileName.toLowerCase().split('.').pop();
|
|
container.innerHTML = `
|
|
<div class="preview-error">
|
|
<p>Prévisualisation non disponible pour ce type de fichier (.${extension})</p>
|
|
<p>Utilisez le bouton "Télécharger" ci-dessous pour obtenir le fichier.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function hidePreview() {
|
|
const previewSection = document.getElementById('preview-section');
|
|
if (previewSection) {
|
|
previewSection.style.display = 'none';
|
|
}
|
|
|
|
currentPreviewFile = { url: '', name: '' };
|
|
}
|
|
|
|
function downloadCurrentFile() {
|
|
if (currentPreviewFile.url) {
|
|
const link = document.createElement('a');
|
|
link.href = currentPreviewFile.url;
|
|
link.download = currentPreviewFile.name;
|
|
link.target = '_blank';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
}
|
|
|
|
// Fonction pour ouvrir automatiquement index.rst s'il existe
|
|
function autoOpenIndexRst(files) {
|
|
// Chercher un fichier index.rst dans la liste
|
|
const indexRstFile = files.find(file =>
|
|
file.name.toLowerCase() === 'index.rst'
|
|
);
|
|
|
|
if (indexRstFile) {
|
|
// Construire l'URL complète du fichier
|
|
const fileUrl = `${config.baseUrl}/${indexRstFile.fullKey}`;
|
|
|
|
// Ouvrir automatiquement la prévisualisation
|
|
console.log('Auto-ouverture de index.rst trouvé:', indexRstFile.name);
|
|
showPreview(fileUrl, indexRstFile.name);
|
|
} else {
|
|
// Pas d'index.rst trouvé, fermer le preview s'il est ouvert
|
|
console.log('Aucun index.rst trouvé, fermeture du preview');
|
|
hidePreview();
|
|
}
|
|
}
|
|
|
|
// Fonction simple de parsing Markdown vers HTML
|
|
function parseMarkdown(text) {
|
|
let html = text;
|
|
|
|
// Headers
|
|
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
|
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
|
|
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
|
|
|
|
// Bold
|
|
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
html = html.replace(/__(.*?)__/g, '<strong>$1</strong>');
|
|
|
|
// Italic
|
|
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
html = html.replace(/_(.*?)_/g, '<em>$1</em>');
|
|
|
|
// Code inline
|
|
html = html.replace(/`(.*?)`/g, '<code>$1</code>');
|
|
|
|
// Code blocks
|
|
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
|
|
|
// Links
|
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
|
|
// Images
|
|
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2" style="max-width: 100%; height: auto;">');
|
|
|
|
// Lists
|
|
html = html.replace(/^\* (.*$)/gim, '<li>$1</li>');
|
|
html = html.replace(/^- (.*$)/gim, '<li>$1</li>');
|
|
html = html.replace(/^\d+\. (.*$)/gim, '<li>$1</li>');
|
|
|
|
// Wrap consecutive <li> items in <ul>
|
|
html = html.replace(/(<li>.*<\/li>)/gs, (match) => {
|
|
return '<ul>' + match + '</ul>';
|
|
});
|
|
|
|
// Line breaks
|
|
html = html.replace(/\n\n/g, '</p><p>');
|
|
html = '<p>' + html + '</p>';
|
|
|
|
// Clean up empty paragraphs
|
|
html = html.replace(/<p><\/p>/g, '');
|
|
html = html.replace(/<p>(<h[1-6]>)/g, '$1');
|
|
html = html.replace(/(<\/h[1-6]>)<\/p>/g, '$1');
|
|
html = html.replace(/<p>(<ul>)/g, '$1');
|
|
html = html.replace(/(<\/ul>)<\/p>/g, '$1');
|
|
html = html.replace(/<p>(<pre>)/g, '$1');
|
|
html = html.replace(/(<\/pre>)<\/p>/g, '$1');
|
|
|
|
return html;
|
|
}
|
|
|
|
// Fonction simple de parsing reStructuredText vers HTML
|
|
function parseRst(text) {
|
|
let html = text;
|
|
|
|
// Créer un système de placeholders pour éviter les liens imbriqués
|
|
const linkPlaceholders = new Map();
|
|
let placeholderIndex = 0;
|
|
|
|
// Fonction pour créer un placeholder unique
|
|
const createLinkPlaceholder = (linkText, linkUrl) => {
|
|
const placeholder = `__LINK_PLACEHOLDER_${placeholderIndex++}__`;
|
|
linkPlaceholders.set(placeholder, `<a href="${linkUrl}" target="_blank">${linkText}</a>`);
|
|
return placeholder;
|
|
};
|
|
|
|
// Headers (RST style with underlines) - ordre important pour éviter les conflits
|
|
html = html.replace(/^(.*)\n#{3,}$/gim, '<h1>$1</h1>');
|
|
html = html.replace(/^(.*)\n={3,}$/gim, '<h2>$1</h2>');
|
|
html = html.replace(/^(.*)\n-{3,}$/gim, '<h3>$1</h3>');
|
|
html = html.replace(/^(.*)\n~{3,}$/gim, '<h4>$1</h4>');
|
|
html = html.replace(/^(.*)\n\^{3,}$/gim, '<h5>$1</h5>');
|
|
|
|
// Metadata fields
|
|
html = html.replace(/^:([^:]+):\s*(.*)$/gim, (match, key, value) => {
|
|
// Remplacer les liens par des placeholders dans les métadonnées
|
|
const processedValue = value.replace(/`([^<]+) <([^>]+)>`_/g, (match, text, url) => {
|
|
return createLinkPlaceholder(text, url);
|
|
});
|
|
return `<div class="rst-metadata"><span class="metadata-key">${key}:</span> <span class="metadata-value">${processedValue}</span></div>`;
|
|
});
|
|
|
|
// Custom directives (.. directive::)
|
|
html = html.replace(/^\.\. ([^:]+)::\s*(.*)$/gim, '<div class="rst-directive rst-directive-$1" data-directive="$1" data-content="$2">');
|
|
|
|
// Directive options (:option: value)
|
|
html = html.replace(/^ :([^:]+):\s*(.*)$/gim, (match, key, value) => {
|
|
// Remplacer les liens par des placeholders dans les options
|
|
const processedValue = value.replace(/`([^<]+) <([^>]+)>`_/g, (match, text, url) => {
|
|
return createLinkPlaceholder(text, url);
|
|
});
|
|
return `<div class="directive-option" data-option="${key}" data-value="${value}"><span class="option-key">${key}:</span> <span class="option-value">${processedValue}</span></div>`;
|
|
});
|
|
|
|
// Special handling for big_button directive
|
|
html = html.replace(/<div class="rst-directive rst-directive-big_button"[^>]*>/g, '<div class="rst-big-button">');
|
|
|
|
// Process directive content blocks (indented text after directive)
|
|
html = html.replace(/(<div class="rst-directive[^>]*>)\n((?: [^\n]*\n?)*)/gim, (match, directive, content) => {
|
|
if (content.trim()) {
|
|
const cleanContent = content.replace(/^ /gm, '').trim();
|
|
// Remplacer les liens par des placeholders dans le contenu
|
|
const processedContent = cleanContent.replace(/`([^<]+) <([^>]+)>`_/g, (match, text, url) => {
|
|
return createLinkPlaceholder(text, url);
|
|
});
|
|
return directive + '<div class="directive-content">' + processedContent + '</div></div>';
|
|
}
|
|
return directive + '</div>';
|
|
});
|
|
|
|
// Bold
|
|
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
|
|
// Italic
|
|
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
|
|
// Code inline
|
|
html = html.replace(/``(.*?)``/g, '<code>$1</code>');
|
|
|
|
// Code blocks (:: at end of line followed by indented block)
|
|
html = html.replace(/::\s*\n\n((?: .*\n?)+)/g, '<pre><code>$1</code></pre>');
|
|
|
|
// Links restants dans le texte principal
|
|
html = html.replace(/`([^<]+) <([^>]+)>`_/g, (match, text, url) => {
|
|
return createLinkPlaceholder(text, url);
|
|
});
|
|
|
|
// Lists
|
|
html = html.replace(/^\* (.*$)/gim, '<li>$1</li>');
|
|
html = html.replace(/^- (.*$)/gim, '<li>$1</li>');
|
|
html = html.replace(/^\d+\. (.*$)/gim, '<li>$1</li>');
|
|
|
|
// Wrap consecutive <li> items in <ul>
|
|
html = html.replace(/(<li>.*<\/li>)/gs, (match) => {
|
|
return '<ul>' + match + '</ul>';
|
|
});
|
|
|
|
// Line breaks
|
|
html = html.replace(/\n\n/g, '</p><p>');
|
|
html = '<p>' + html + '</p>';
|
|
|
|
// Clean up empty paragraphs and fix structure
|
|
html = html.replace(/<p><\/p>/g, '');
|
|
html = html.replace(/<p>(<h[1-6]>)/g, '$1');
|
|
html = html.replace(/(<\/h[1-6]>)<\/p>/g, '$1');
|
|
html = html.replace(/<p>(<div class="rst-)/g, '<div class="rst-');
|
|
html = html.replace(/(<\/div>)<\/p>/g, '$1');
|
|
html = html.replace(/<p>(<ul>)/g, '$1');
|
|
html = html.replace(/(<\/ul>)<\/p>/g, '$1');
|
|
html = html.replace(/<p>(<pre>)/g, '$1');
|
|
html = html.replace(/(<\/pre>)<\/p>/g, '$1');
|
|
|
|
// Restaurer tous les liens à partir des placeholders
|
|
for (const [placeholder, linkHtml] of linkPlaceholders) {
|
|
html = html.replace(new RegExp(placeholder, 'g'), linkHtml);
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
function showMarkdownPreview(fileUrl, container) {
|
|
fetch(fileUrl)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
})
|
|
.then(text => {
|
|
const htmlContent = parseMarkdown(text);
|
|
const div = document.createElement('div');
|
|
div.className = 'markdown-content';
|
|
div.innerHTML = htmlContent;
|
|
container.innerHTML = '';
|
|
container.appendChild(div);
|
|
})
|
|
.catch(error => {
|
|
console.error('Erreur chargement markdown:', error);
|
|
container.innerHTML = '<div class="preview-error">Erreur: Impossible de charger le fichier Markdown</div>';
|
|
});
|
|
}
|
|
|
|
function showRstPreview(fileUrl, container) {
|
|
fetch(fileUrl)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
})
|
|
.then(text => {
|
|
const htmlContent = parseRst(text);
|
|
const div = document.createElement('div');
|
|
div.className = 'rst-content';
|
|
div.innerHTML = htmlContent;
|
|
container.innerHTML = '';
|
|
container.appendChild(div);
|
|
})
|
|
.catch(error => {
|
|
console.error('Erreur chargement RST:', error);
|
|
container.innerHTML = '<div class="preview-error">Erreur: Impossible de charger le fichier reStructuredText</div>';
|
|
});
|
|
} |