36 KiB
36 KiB
Руководство по работе с Vditor
Содержание
- Введение
- Инициализация редактора
- Работа с API редактора
- Сохранение и загрузка контента
- Работа с HTML и Markdown
- Отображение контента без редактора
- Хранение в базе данных
- Примеры использования
Введение
Vditor - это мощный WYSIWYG Markdown редактор с поддержкой LaTeX формул через KaTeX. Редактор предоставляет богатый API для работы с контентом.
Инициализация редактора
Базовый пример инициализации
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 инициализирован');
}
});
}
Русская локализация
// Русская локализация для подсказок
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
// Получение текущего содержимого редактора
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
};
}
Обработка событий
// Добавление обработчиков событий
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
// Пример сохранения контента на сервер
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;
}
}
Загрузка контента из базы данных
// Загрузка контента по 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;
}
}
Работа с черновиками
// Управление черновиками
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
Конвертация между форматами
// Конвертация 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 формул
// Извлечение 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
// Функция для отображения контента без редактора
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;
}