1039 lines
36 KiB
Markdown
1039 lines
36 KiB
Markdown
# Руководство по работе с Vditor
|
||
## Содержание
|
||
1. [Введение](#введение)
|
||
2. [Инициализация редактора](#инициализация-редактора)
|
||
3. [Работа с API редактора](#работа-с-api-редактора)
|
||
4. [Сохранение и загрузка контента](#сохранение-и-загрузка-контента)
|
||
5. [Работа с HTML и Markdown](#работа-с-html-и-markdown)
|
||
6. [Отображение контента без редактора](#отображение-контента-без-редактора)
|
||
7. [Хранение в базе данных](#хранение-в-базе-данных)
|
||
8. [Примеры использования](#примеры-использования)
|
||
## Введение
|
||
Vditor - это мощный WYSIWYG Markdown редактор с поддержкой LaTeX формул через KaTeX. Редактор предоставляет богатый API для работы с контентом.
|
||
|
||
## Инициализация редактора
|
||
|
||
### Базовый пример инициализации
|
||
|
||
```javascript
|
||
let vditorInstance = null;
|
||
function initVditor() {
|
||
// Загрузка скрипта Vditor если не загружен
|
||
if (typeof Vditor === 'undefined') {
|
||
loadVditor().then(() => initVditorEditor());
|
||
} else {
|
||
initVditorEditor();
|
||
}
|
||
}
|
||
function loadVditor() {
|
||
return new Promise((resolve, reject) => {
|
||
if (typeof Vditor !== 'undefined') {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = 'https://unpkg.com/vditor@3.10.4/dist/index.min.js';
|
||
script.onload = resolve;
|
||
script.onerror = reject;
|
||
document.head.appendChild(script);
|
||
});
|
||
}
|
||
function initVditorEditor(initialContent = '') {
|
||
vditorInstance = new Vditor('vditor-container', {
|
||
height: 500,
|
||
placeholder: 'Начните писать здесь...',
|
||
theme: 'classic',
|
||
icon: 'material',
|
||
typewriterMode: true,
|
||
lang: 'ru_RU',
|
||
toolbar: [
|
||
'emoji', 'headings', 'bold', 'italic', 'strike', 'link', '|',
|
||
'list', 'ordered-list', 'check', 'outdent', 'indent', '|',
|
||
'quote', 'line', 'code', 'inline-code', 'insert-before', 'insert-after', '|',
|
||
'upload', 'table', '|',
|
||
'undo', 'redo', '|',
|
||
'edit-mode', 'both', 'preview', 'fullscreen', 'outline',
|
||
'code-theme', 'content-theme', 'export', 'help'
|
||
],
|
||
preview: {
|
||
delay: 500,
|
||
hljs: {
|
||
enable: true,
|
||
style: 'github',
|
||
lineNumber: true
|
||
},
|
||
math: {
|
||
engine: 'KaTeX',
|
||
inlineDigit: true
|
||
},
|
||
markdown: {
|
||
toc: true,
|
||
mark: true,
|
||
footnotes: true,
|
||
autoSpace: true
|
||
}
|
||
},
|
||
value: initialContent, // Начальное содержимое
|
||
cache: {
|
||
enable: false, // Отключаем локальное кэширование
|
||
},
|
||
after: () => {
|
||
console.log('Vditor инициализирован');
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
### Русская локализация
|
||
|
||
```javascript
|
||
// Русская локализация для подсказок
|
||
const ruLang = {
|
||
hint: {
|
||
emoji: 'Эмодзи',
|
||
headings: 'Заголовки',
|
||
bold: 'Жирный',
|
||
italic: 'Курсив',
|
||
strike: 'Зачеркнутый',
|
||
link: 'Ссылка',
|
||
'list': 'Маркированный список',
|
||
'ordered-list': 'Нумерованный список',
|
||
check: 'Список задач',
|
||
outdent: 'Уменьшить отступ',
|
||
indent: 'Увеличить отступ',
|
||
quote: 'Цитата',
|
||
line: 'Разделитель',
|
||
code: 'Блок кода',
|
||
'inline-code': 'Встроенный код',
|
||
'insert-after': 'Вставить строку после',
|
||
'insert-before': 'Вставить строку перед',
|
||
upload: 'Загрузить',
|
||
table: 'Таблица',
|
||
undo: 'Отменить',
|
||
redo: 'Повторить',
|
||
'edit-mode': 'Режим редактирования',
|
||
both: 'Редактирование и просмотр',
|
||
preview: 'Только просмотр',
|
||
fullscreen: 'Полный экран',
|
||
outline: 'Оглавление',
|
||
'code-theme': 'Тема кода',
|
||
'content-theme': 'Тема контента',
|
||
export: 'Экспорт',
|
||
help: 'Помощь'
|
||
},
|
||
toolbar: {
|
||
more: 'Еще'
|
||
}
|
||
};
|
||
// Использование в конфигурации
|
||
vditorInstance = new Vditor('vditor-container', {
|
||
// ... другие настройки
|
||
lang: 'ru_RU',
|
||
// ... остальные настройки
|
||
});
|
||
```
|
||
## Работа с API редактора
|
||
|
||
### Основные методы API
|
||
|
||
```javascript
|
||
// Получение текущего содержимого редактора
|
||
function getEditorContent() {
|
||
if (!vditorInstance) return '';
|
||
return vditorInstance.getValue();
|
||
}
|
||
// Установка содержимого редактора
|
||
function setEditorContent(content) {
|
||
if (!vditorInstance) return;
|
||
vditorInstance.setValue(content);
|
||
}
|
||
// Получение HTML версии контента
|
||
function getEditorHTML() {
|
||
if (!vditorInstance) return '';
|
||
return vditorInstance.getHTML();
|
||
}
|
||
// Получение чистого Markdown (без HTML тегов)
|
||
function getEditorMarkdown() {
|
||
if (!vditorInstance) return '';
|
||
return vditorInstance.getValue();
|
||
}
|
||
// Переключение режима предпросмотра
|
||
function togglePreview() {
|
||
if (!vditorInstance) return;
|
||
vditorInstance.setPreviewMode(vditorInstance.vditor.preview.element.style.display === 'none');
|
||
}
|
||
// Получение выделенного текста
|
||
function getSelectedText() {
|
||
if (!vditorInstance) return '';
|
||
return vditorInstance.getSelection();
|
||
}
|
||
// Вставка текста в текущую позицию курсора
|
||
function insertTextAtCursor(text) {
|
||
if (!vditorInstance) return;
|
||
vditorInstance.insertValue(text);
|
||
}
|
||
// Очистка редактора
|
||
function clearEditor() {
|
||
if (!vditorInstance) return;
|
||
vditorInstance.setValue('');
|
||
}
|
||
// Получение информации о редакторе
|
||
function getEditorInfo() {
|
||
if (!vditorInstance) return null;
|
||
return {
|
||
wordCount: vditorInstance.vditor.undo.undo.length,
|
||
isPreview: vditorInstance.vditor.preview.element.style.display !== 'none',
|
||
isFullscreen: document.fullscreenElement !== null,
|
||
theme: vditorInstance.vditor.options.theme
|
||
};
|
||
}
|
||
```
|
||
|
||
### Обработка событий
|
||
|
||
```javascript
|
||
// Добавление обработчиков событий
|
||
function setupEditorEvents() {
|
||
if (!vditorInstance) return;
|
||
|
||
// Событие изменения содержимого
|
||
vditorInstance.vditor.undo.addListener('stack', (stack) => {
|
||
console.log('Контент изменен:', stack);
|
||
// Автосохранение или валидация
|
||
autoSaveContent();
|
||
});
|
||
|
||
// Событие загрузки файлов
|
||
vditorInstance.vditor.upload.element.addEventListener('change', (e) => {
|
||
console.log('Файлы выбраны:', e.target.files);
|
||
});
|
||
|
||
// Событие переключения режима
|
||
vditorInstance.vditor.preview.element.addEventListener('click', () => {
|
||
console.log('Режим предпросмотра изменен');
|
||
});
|
||
}
|
||
// Автосохранение контента
|
||
let autoSaveTimeout = null;
|
||
function autoSaveContent() {
|
||
if (autoSaveTimeout) {
|
||
clearTimeout(autoSaveTimeout);
|
||
}
|
||
|
||
autoSaveTimeout = setTimeout(() => {
|
||
const content = getEditorContent();
|
||
if (content.trim()) {
|
||
saveToLocalStorage(content);
|
||
console.log('Автосохранение выполнено');
|
||
}
|
||
}, 2000); // Сохраняем каждые 2 секунды после последнего изменения
|
||
}
|
||
function saveToLocalStorage(content) {
|
||
const draft = {
|
||
content: content,
|
||
savedAt: new Date().toISOString(),
|
||
wordCount: content.split(/\\\\s+/).length
|
||
};
|
||
localStorage.setItem('editor_draft', JSON.stringify(draft));
|
||
}
|
||
```
|
||
|
||
## Сохранение и загрузка контента
|
||
|
||
### Сохранение через API
|
||
|
||
```javascript
|
||
// Пример сохранения контента на сервер
|
||
async function saveContentToServer() {
|
||
if (!vditorInstance) {
|
||
throw new Error('Редактор не инициализирован');
|
||
}
|
||
|
||
const content = {
|
||
markdown: vditorInstance.getValue(), // Markdown версия
|
||
html: vditorInstance.getHTML(), // HTML версия
|
||
title: document.getElementById('title-input').value,
|
||
metadata: {
|
||
wordCount: vditorInstance.getValue().split(/\\\\s+/).length,
|
||
created: new Date().toISOString(),
|
||
updated: new Date().toISOString()
|
||
}
|
||
};
|
||
|
||
try {
|
||
// Отправка на сервер
|
||
const response = await fetch('/api/content/save', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${getAuthToken()}`
|
||
},
|
||
body: JSON.stringify(content)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Ошибка сохранения: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('Контент сохранен:', result);
|
||
return result;
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка при сохранении:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
// Сохранение с обработкой прогресса
|
||
async function saveWithProgress() {
|
||
const saveButton = document.getElementById('save-btn');
|
||
const originalText = saveButton.textContent;
|
||
|
||
try {
|
||
saveButton.disabled = true;
|
||
saveButton.textContent = 'Сохранение...';
|
||
|
||
const result = await saveContentToServer();
|
||
|
||
saveButton.textContent = 'Сохранено!';
|
||
setTimeout(() => {
|
||
saveButton.textContent = originalText;
|
||
saveButton.disabled = false;
|
||
}, 2000);
|
||
|
||
return result;
|
||
|
||
} catch (error) {
|
||
saveButton.textContent = 'Ошибка!';
|
||
saveButton.classList.add('error');
|
||
|
||
setTimeout(() => {
|
||
saveButton.textContent = originalText;
|
||
saveButton.classList.remove('error');
|
||
saveButton.disabled = false;
|
||
}, 3000);
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Загрузка контента из базы данных
|
||
|
||
```javascript
|
||
// Загрузка контента по ID
|
||
async function loadContentFromServer(contentId) {
|
||
try {
|
||
const response = await fetch(`/api/content/${contentId}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${getAuthToken()}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Ошибка загрузки: ${response.status}`);
|
||
}
|
||
|
||
const content = await response.json();
|
||
|
||
// Установка контента в редактор
|
||
if (vditorInstance) {
|
||
vditorInstance.setValue(content.markdown || content.content || '');
|
||
|
||
// Обновление дополнительных полей если есть
|
||
if (content.title) {
|
||
document.getElementById('title-input').value = content.title;
|
||
}
|
||
|
||
console.log('Контент загружен:', content);
|
||
}
|
||
|
||
return content;
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка при загрузке:', error);
|
||
|
||
// Загрузка из локального хранилища при ошибке
|
||
const localDraft = localStorage.getItem(`content_draft_${contentId}`);
|
||
if (localDraft && vditorInstance) {
|
||
try {
|
||
const draft = JSON.parse(localDraft);
|
||
vditorInstance.setValue(draft.content || '');
|
||
console.log('Загружен локальный черновик');
|
||
} catch (e) {
|
||
console.error('Ошибка загрузки черновика:', e);
|
||
}
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Загрузка с индикацией прогресса
|
||
async function loadWithProgress(contentId) {
|
||
const editorContainer = document.getElementById('vditor-container');
|
||
const loadingIndicator = document.createElement('div');
|
||
|
||
loadingIndicator.className = 'loading-indicator';
|
||
loadingIndicator.innerHTML = `
|
||
<div class=\\\"spinner\\\"></div>
|
||
<p>Загрузка контента...</p>
|
||
`;
|
||
|
||
editorContainer.appendChild(loadingIndicator);
|
||
|
||
try {
|
||
const content = await loadContentFromServer(contentId);
|
||
loadingIndicator.remove();
|
||
return content;
|
||
|
||
} catch (error) {
|
||
loadingIndicator.innerHTML = `
|
||
<div class=\\\"error-icon\\\">⚠️</div>
|
||
<p>Ошибка загрузки. Используется локальная версия.</p>
|
||
`;
|
||
|
||
setTimeout(() => {
|
||
loadingIndicator.remove();
|
||
}, 3000);
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Работа с черновиками
|
||
|
||
```javascript
|
||
// Управление черновиками
|
||
class DraftManager {
|
||
constructor(editorInstance, draftKey = 'editor_draft') {
|
||
this.editor = editorInstance;
|
||
this.draftKey = draftKey;
|
||
this.autoSaveInterval = null;
|
||
this.isDirty = false;
|
||
}
|
||
|
||
// Включение автосохранения
|
||
enableAutoSave(interval = 30000) { // 30 секунд
|
||
if (this.autoSaveInterval) {
|
||
clearInterval(this.autoSaveInterval);
|
||
}
|
||
|
||
this.autoSaveInterval = setInterval(() => {
|
||
if (this.isDirty) {
|
||
this.saveDraft();
|
||
this.isDirty = false;
|
||
}
|
||
}, interval);
|
||
|
||
// Отслеживание изменений
|
||
if (this.editor) {
|
||
this.editor.vditor.undo.addListener('stack', () => {
|
||
this.isDirty = true;
|
||
});
|
||
}
|
||
}
|
||
|
||
// Отключение автосохранения
|
||
disableAutoSave() {
|
||
if (this.autoSaveInterval) {
|
||
clearInterval(this.autoSaveInterval);
|
||
this.autoSaveInterval = null;
|
||
}
|
||
}
|
||
|
||
// Сохранение черновика
|
||
saveDraft(additionalData = {}) {
|
||
if (!this.editor) return null;
|
||
|
||
const draft = {
|
||
content: this.editor.getValue(),
|
||
html: this.editor.getHTML(),
|
||
savedAt: new Date().toISOString(),
|
||
metadata: {
|
||
wordCount: this.editor.getValue().split(/\\\\s+/).length,
|
||
...additionalData
|
||
}
|
||
};
|
||
|
||
localStorage.setItem(this.draftKey, JSON.stringify(draft));
|
||
console.log('Черновик сохранен:', draft);
|
||
return draft;
|
||
}
|
||
|
||
// Загрузка черновика
|
||
loadDraft() {
|
||
const draftJson = localStorage.getItem(this.draftKey);
|
||
if (!draftJson) return null;
|
||
|
||
try {
|
||
const draft = JSON.parse(draftJson);
|
||
|
||
if (this.editor && draft.content) {
|
||
this.editor.setValue(draft.content);
|
||
console.log('Черновик загружен:', draft);
|
||
}
|
||
|
||
return draft;
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки черновика:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Очистка черновика
|
||
clearDraft() {
|
||
localStorage.removeItem(this.draftKey);
|
||
this.isDirty = false;
|
||
console.log('Черновик очищен');
|
||
}
|
||
|
||
// Получение информации о черновике
|
||
getDraftInfo() {
|
||
const draftJson = localStorage.getItem(this.draftKey);
|
||
if (!draftJson) return null;
|
||
|
||
try {
|
||
const draft = JSON.parse(draftJson);
|
||
return {
|
||
exists: true,
|
||
savedAt: new Date(draft.savedAt),
|
||
wordCount: draft.metadata?.wordCount || 0,
|
||
size: new Blob([draftJson]).size
|
||
};
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
// Использование менеджера черновиков
|
||
const draftManager = new DraftManager(vditorInstance, 'course_content_draft');
|
||
// Включение автосохранения при инициализации
|
||
draftManager.enableAutoSave();
|
||
// Ручное сохранение
|
||
document.getElementById('save-draft-btn').addEventListener('click', () => {
|
||
const title = document.getElementById('course-title').value;
|
||
draftManager.saveDraft({ title: title });
|
||
});
|
||
```
|
||
|
||
|
||
## Работа с HTML и Markdown
|
||
|
||
### Конвертация между форматами
|
||
|
||
```javascript
|
||
// Конвертация Markdown в HTML с поддержкой KaTeX
|
||
function convertMarkdownToHTML(markdown) {
|
||
if (!markdown) return '';
|
||
|
||
// Используем встроенные возможности Vditor для конвертации
|
||
if (vditorInstance && vditorInstance.vditor) {
|
||
try {
|
||
// Vditor использует Lute для парсинга Markdown
|
||
const html = vditorInstance.vditor.lute.Md2HTML(markdown);
|
||
return html;
|
||
} catch (error) {
|
||
console.warn('Ошибка конвертации через Lute:', error);
|
||
}
|
||
}
|
||
|
||
// Fallback: используем marked.js если Vditor не доступен
|
||
if (typeof marked !== 'undefined') {
|
||
// Настройка marked для поддержки LaTeX
|
||
marked.setOptions({
|
||
breaks: true,
|
||
gfm: true,
|
||
highlight: function(code, lang) {
|
||
if (lang && hljs.getLanguage(lang)) {
|
||
return hljs.highlight(code, { language: lang }).value;
|
||
}
|
||
return hljs.highlightAuto(code).value;
|
||
}
|
||
});
|
||
|
||
// Обработка LaTeX формул
|
||
const processedMarkdown = markdown.replace(/\\\\$\\\\$(.*?)\\\\$\\\\$/g, (match, formula) => {
|
||
try {
|
||
return katex.renderToString(formula, { displayMode: true });
|
||
} catch (e) {
|
||
return `<div class=\\\"latex-error\\\">Ошибка формулы: ${formula}</div>`;
|
||
}
|
||
}).replace(/\\\\$(.*?)\\\\$/g, (match, formula) => {
|
||
try {
|
||
return katex.renderToString(formula, { displayMode: false });
|
||
} catch (e) {
|
||
return `<span class=\\\"latex-error\\\">${formula}</span>`;
|
||
}
|
||
});
|
||
|
||
return marked(processedMarkdown);
|
||
}
|
||
|
||
// Самый простой fallback
|
||
return markdown
|
||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||
.replace(/\\\\*\\\\*(.*?)\\\\*\\\\*/g, '<strong>$1</strong>')
|
||
.replace(/\\\\*(.*?)\\\\*/g, '<em>$1</em>')
|
||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||
.replace(/\\
|
||
/g, '<br>');
|
||
}
|
||
// Конвертация HTML в Markdown (упрощенная)
|
||
function convertHTMLToMarkdown(html) {
|
||
if (!html) return '';
|
||
|
||
// Простая конвертация основных тегов
|
||
let markdown = html
|
||
.replace(/<h1>(.*?)<\\\\/h1>/gi, '# $1\\
|
||
\\
|
||
')
|
||
.replace(/<h2>(.*?)<\\\\/h2>/gi, '## $1\\
|
||
\\
|
||
')
|
||
.replace(/<h3>(.*?)<\\\\/h3>/gi, '### $1\\
|
||
\\
|
||
')
|
||
.replace(/<strong>(.*?)<\\\\/strong>/gi, '**$1**')
|
||
.replace(/<b>(.*?)<\\\\/b>/gi, '**$1**')
|
||
.replace(/<em>(.*?)<\\\\/em>/gi, '*$1*')
|
||
.replace(/<i>(.*?)<\\\\/i>/gi, '*$1*')
|
||
.replace(/<code>(.*?)<\\\\/code>/gi, '\`$1\`')
|
||
.replace(/<pre><code>(.*?)<\\\\/code><\\\\/pre>/gis, '\`\``\\
|
||
$1\\
|
||
```\\
|
||
')
|
||
.replace(/<br\\\\s*\\\\/?>/gi, '\\
|
||
')
|
||
.replace(/<p>(.*?)<\\\\/p>/gi, '$1\\
|
||
\\
|
||
')
|
||
.replace(/<ul>(.*?)<\\\\/ul>/gis, (match, content) => {
|
||
return content.replace(/<li>(.*?)<\\\\/li>/gi, '- $1\\
|
||
');
|
||
})
|
||
.replace(/<ol>(.*?)<\\\\/ol>/gis, (match, content) => {
|
||
let counter = 1;
|
||
return content.replace(/<li>(.*?)<\\\\/li>/gi, () => `${counter++}. $1\\
|
||
`);
|
||
})
|
||
.replace(/<a href=\\\"(.*?)\\\">(.*?)<\\\\/a>/gi, '[$2]($1)')
|
||
.replace(/<[^>]*>/g, '') // Удаление остальных тегов
|
||
.replace(/ /g, ' ')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/&/g, '&')
|
||
.replace(/\\
|
||
{3,}/g, '\\
|
||
\\
|
||
'); // Удаление лишних переносов
|
||
|
||
return markdown.trim();
|
||
}
|
||
// Получение обеих версий контента
|
||
function getContentInBothFormats() {
|
||
if (!vditorInstance) return { markdown: '', html: '' };
|
||
|
||
const markdown = vditorInstance.getValue();
|
||
const html = vditorInstance.getHTML();
|
||
|
||
return {
|
||
markdown: markdown,
|
||
html: html,
|
||
stats: {
|
||
markdownLength: markdown.length,
|
||
htmlLength: html.length,
|
||
wordCount: markdown.split(/\\\\s+/).length,
|
||
lineCount: markdown.split('\\
|
||
').length
|
||
}
|
||
};
|
||
}
|
||
// Экспорт контента в разные форматы
|
||
function exportContent(format = 'both') {
|
||
if (!vditorInstance) return null;
|
||
|
||
const content = getContentInBothFormats();
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const filename = `content-${timestamp}`;
|
||
|
||
switch (format) {
|
||
case 'markdown':
|
||
downloadFile(`${filename}.md`, content.markdown, 'text/markdown');
|
||
break;
|
||
|
||
case 'html':
|
||
downloadFile(`${filename}.html`, `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset=\\\"UTF-8\\\">
|
||
<title>Экспортированный контент</title>
|
||
<link rel=\\\"stylesheet\\\" href=\\\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css\\\">
|
||
<link rel=\\\"stylesheet\\\" href=\\\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css\\\">
|
||
<style>
|
||
body { font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; max-width: 800px; margin: 0 auto; }
|
||
.latex-formula { overflow-x: auto; padding: 10px; background: #f5f5f5; }
|
||
pre code { border-radius: 4px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
${content.html}
|
||
<script src=\\\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js\\\"></script>
|
||
<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\\\"></script>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Рендеринг LaTeX формул
|
||
document.querySelectorAll('.language-math').forEach(el => {
|
||
try {
|
||
katex.render(el.textContent, el, {
|
||
throwOnError: false,
|
||
displayMode: el.classList.contains('display')
|
||
});
|
||
} catch (e) {
|
||
console.error('Ошибка рендеринга формулы:', e);
|
||
}
|
||
});
|
||
|
||
// Подсветка синтаксиса
|
||
hljs.highlightAll();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`, 'text/html');
|
||
break;
|
||
|
||
case 'both':
|
||
downloadFile(`${filename}.md`, content.markdown, 'text/markdown');
|
||
downloadFile(`${filename}.html`, content.html, 'text/html');
|
||
break;
|
||
|
||
case 'json':
|
||
downloadFile(`${filename}.json`, JSON.stringify(content, null, 2), 'application/json');
|
||
break;
|
||
}
|
||
|
||
return content;
|
||
}
|
||
// Вспомогательная функция для скачивания файлов
|
||
function downloadFile(filename, content, mimeType) {
|
||
const blob = new Blob([content], { type: mimeType });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
```
|
||
|
||
### Обработка LaTeX формул
|
||
```javascript
|
||
// Извлечение LaTeX формул из контента
|
||
function extractLatexFormulas(content) {
|
||
const formulas = {
|
||
inline: [],
|
||
display: []
|
||
};
|
||
|
||
// Поиск inline формул: $...$
|
||
const inlineRegex = /\\\\$(.*?)\\\\$/g;
|
||
let match;
|
||
while ((match = inlineRegex.exec(content)) !== null) {
|
||
formulas.inline.push({
|
||
formula: match[1],
|
||
position: match.index,
|
||
length: match[0].length
|
||
});
|
||
}
|
||
|
||
// Поиск display формул: $$...$$
|
||
const displayRegex = /\\\\$\\\\$(.*?)\\\\$\\\\$/g;
|
||
while ((match = displayRegex.exec(content)) !== null) {
|
||
formulas.display.push({
|
||
formula: match[1],
|
||
position: match.index,
|
||
length: match[0].length
|
||
});
|
||
}
|
||
|
||
return formulas;
|
||
}
|
||
// Валидация LaTeX формул
|
||
function validateLatexFormulas(content) {
|
||
const formulas = extractLatexFormulas(content);
|
||
const errors = [];
|
||
|
||
formulas.inline.forEach((formula, index) => {
|
||
try {
|
||
// Проверка синтаксиса KaTeX
|
||
katex.__parse(formula.formula);
|
||
} catch (error) {
|
||
errors.push({
|
||
type: 'inline',
|
||
index: index,
|
||
formula: formula.formula,
|
||
error: error.message,
|
||
position: formula.position
|
||
});
|
||
}
|
||
});
|
||
|
||
formulas.display.forEach((formula, index) => {
|
||
try {
|
||
katex.__parse(formula.formula);
|
||
} catch (error) {
|
||
errors.push({
|
||
type: 'display',
|
||
index: index,
|
||
formula: formula.formula,
|
||
error: error.message,
|
||
position: formula.position
|
||
});
|
||
}
|
||
});
|
||
|
||
return {
|
||
valid: errors.length === 0,
|
||
totalFormulas: formulas.inline.length + formulas.display.length,
|
||
errors: errors
|
||
};
|
||
}
|
||
// Автоматическое исправление распространенных ошибок LaTeX
|
||
function fixCommonLatexErrors(content) {
|
||
let fixedContent = content;
|
||
|
||
// Замена русских букв в математическом режиме
|
||
fixedContent = fixedContent.replace(/\\\\$([^$]*?)[а-яА-Я]([^$]*?)\\\\$/g, (match, before, after) => {
|
||
console.warn('Обнаружены русские буквы в формуле:', match);
|
||
return `$${before}\\\\\\\\text{...}${after}$`;
|
||
});
|
||
|
||
// Добавление пропущенных обратных слешей
|
||
fixedContent = fixedContent.replace(/\\\\$(.*?)(alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega)(.*?)\\\\$/gi,
|
||
(match, before, greek, after) => `$${before}\\\\\\\\${greek}${after}$`);
|
||
|
||
// Исправление дробей без frac
|
||
fixedContent = fixedContent.replace(/\\\\$(.*?)(\\\\d+)\\\\/(\\\\d+)(.*?)\\\\$/g, (match, before, num, den, after) => {
|
||
return `$${before}\\\\\\\\frac{${num}}{${den}}${after}$`;
|
||
});
|
||
|
||
return fixedContent;
|
||
}
|
||
```
|
||
|
||
## Отображение контента без редактора
|
||
### Рендеринг HTML с поддержкой KaTeX
|
||
|
||
```javascript
|
||
// Функция для отображения контента без редактора
|
||
function renderContentWithoutEditor(content, containerId, options = {}) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) {
|
||
console.error(`Контейнер ${containerId} не найден`);
|
||
return;
|
||
}
|
||
|
||
const config = {
|
||
enableMath: true,
|
||
enableSyntaxHighlight: true,
|
||
theme: 'light',
|
||
...options
|
||
};
|
||
|
||
// Конвертация Markdown в HTML если нужно
|
||
let htmlContent;
|
||
if (content.markdown) {
|
||
htmlContent = convertMarkdownToHTML(content.markdown);
|
||
} else if (content.html) {
|
||
htmlContent = content.html;
|
||
} else {
|
||
htmlContent = convertMarkdownToHTML(content);
|
||
}
|
||
|
||
// Создание структуры для отображения
|
||
container.innerHTML = `
|
||
<div class=\\\"rendered-content ${config.theme}-theme\\\">
|
||
<div class=\\\"content-body\\\">
|
||
${htmlContent}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Инициализация KaTeX для рендеринга формул
|
||
if (config.enableMath && typeof katex !== 'undefined') {
|
||
renderLatexFormulas(container);
|
||
}
|
||
|
||
// Подсветка синтаксиса
|
||
if (config.enableSyntaxHighlight && typeof hljs !== 'undefined') {
|
||
hljs.highlightAll();
|
||
}
|
||
|
||
// Добавление стилей если их нет
|
||
addContentStyles();
|
||
}
|
||
// Рендеринг LaTeX формул в контейнере
|
||
function renderLatexFormulas(container) {
|
||
if (!container || typeof katex === 'undefined') return;
|
||
|
||
// Рендеринг display формул ($$...$$)
|
||
container.querySelectorAll('.language-math.display, .katex-display').forEach(element => {
|
||
try {
|
||
const formula = element.textContent.trim();
|
||
katex.render(formula, element, {
|
||
displayMode: true,
|
||
throwOnError: false,
|
||
errorColor: '#cc0000'
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка рендеринга display формулы:', error);
|
||
element.innerHTML = `<div class=\\\"latex-error\\\">Ошибка формулы: ${element.textContent}</div>`;
|
||
}
|
||
});
|
||
|
||
// Рендеринг inline формул ($...$)
|
||
container.querySelectorAll('.language-math:not(.display), .katex').forEach(element => {
|
||
try {
|
||
const formula = element.textContent.trim();
|
||
katex.render(formula, element, {
|
||
displayMode: false,
|
||
throwOnError: false,
|
||
errorColor: '#cc0000'
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка рендеринга inline формулы:', error);
|
||
element.innerHTML = `<span class=\\\"latex-error\\\">${element.textContent}</span>`;
|
||
}
|
||
});
|
||
|
||
// Обработка формул в тексте (если они не обернуты в теги)
|
||
const textNodes = Array.from(container.childNodes).filter(node =>
|
||
node.nodeType === Node.TEXT_NODE &&
|
||
(node.textContent.includes('$') || node.textContent.includes('\\\\\\\\['))
|
||
);
|
||
|
||
textNodes.forEach(node => {
|
||
const wrapper = document.createElement('span');
|
||
wrapper.innerHTML = node.textContent
|
||
.replace(/\\\\$\\\\$(.*?)\\\\$\\\\$/g, (match, formula) => {
|
||
try {
|
||
return katex.renderToString(formula, { displayMode: true });
|
||
} catch (e) {
|
||
return `<div class=\\\"latex-error\\\">Ошибка: ${formula}</div>`;
|
||
}
|
||
})
|
||
.replace(/\\\\$(.*?)\\\\$/g, (match, formula) => {
|
||
try {
|
||
return katex.renderToString(formula, { displayMode: false });
|
||
} catch (e) {
|
||
return `<span class=\\\"latex-error\\\">${formula}</span>`;
|
||
}
|
||
});
|
||
|
||
node.parentNode.replaceChild(wrapper, node);
|
||
});
|
||
}
|
||
// Добавление необходимых стилей
|
||
function addContentStyles() {
|
||
if (document.getElementById('content-render-styles')) return;
|
||
|
||
const styles = `
|
||
<style id=\\\"content-render-styles\\\">
|
||
.rendered-content {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
}
|
||
|
||
.rendered-content.light-theme {
|
||
background: #fff;
|
||
color: #333;
|
||
}
|
||
|
||
.rendered-content.dark-theme {
|
||
background: #1a1a1a;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.content-body {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.content-body h1,
|
||
.content-body h2,
|
||
.content-body h3,
|
||
.content-body h4 {
|
||
margin-top: 1.5em;
|
||
margin-bottom: 0.5em;
|
||
font-weight: 600;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.content-body h1 { font-size: 2em; border-bottom: 2px solid #eee; padding-bottom: 0.3em; }
|
||
.content-body h2 { font-size: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
|
||
.content-body h3 { font-size: 1.25em; }
|
||
.content-body h4 { font-size: 1em; }
|
||
|
||
.content-body p {
|
||
margin: 1em 0;
|
||
}
|
||
|
||
.content-body ul,
|
||
.content-body ol {
|
||
padding-left: 2em;
|
||
margin: 1em 0;
|
||
}
|
||
|
||
.content-body li {
|
||
margin: 0.5em 0;
|
||
}
|
||
|
||
.content-body blockquote {
|
||
margin: 1em 0;
|
||
padding: 0.5em 1em;
|
||
border-left: 4px solid #ddd;
|
||
background: #f9f9f9;
|
||
color: #666;
|
||
}
|
||
|
||
.content-body pre {
|
||
background: #f6f8fa;
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
overflow: auto;
|
||
margin: 1em 0;
|
||
}
|
||
|
||
.content-body code {
|
||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||
padding: 0.2em 0.4em;
|
||
background: rgba(175, 184, 193, 0.2);
|
||
border-radius: 3px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.content-body pre code {
|
||
background: transparent;
|
||
padding: 0;
|
||
}
|
||
|
||
.content-body table {
|
||
border-collapse: collapse;
|
||
width: 100%;
|
||
margin: 1em 0;
|
||
}
|
||
```
|