Files
pyisu/veditor.md
2026-03-13 14:39:43 +08:00

1039 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Руководство по работе с 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(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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;
}
```