37 KiB
37 KiB
Руководство по работе с Vditor
6. Отображение контента без редактора
6.1 Рендеринг 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;
}
.content-body th,
.content-body td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.content-body th {
background: #f6f8fa;
font-weight: 600;
}
.content-body tr:nth-child(even) {
background: #f9f9f9;
}
.latex-error {
color: #d73a49;
background: #ffebee;
padding: 4px 8px;
border-radius: 3px;
font-family: monospace;
}
.katex-display {
overflow-x: auto;
overflow-y: hidden;
padding: 10px 0;
}
.katex {
font-size: 1.1em;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', styles);
}
6.2 Пример использования
// Пример 1: Отображение сохраненного контента
async function displaySavedContent(contentId) {
try {
// Загрузка контента из базы данных
const response = await fetch(`/api/content/${contentId}`);
const content = await response.json();
// Отображение в контейнере
renderContentWithoutEditor(content, 'content-container', {
theme: 'light',
enableMath: true,
enableSyntaxHighlight: true
});
} catch (error) {
console.error('Ошибка загрузки контента:', error);
document.getElementById('content-container').innerHTML = `
<div class=\\\"error-message\\\">
<h3>Ошибка загрузки контента</h3>
<p>${error.message}</p>
</div>
`;
}
}
// Пример 2: Предпросмотр перед сохранением
function previewContent() {
if (!vditorInstance) return;
const content = {
markdown: vditorInstance.getValue(),
html: vditorInstance.getHTML()
};
// Открытие предпросмотра в модальном окне
const modal = document.createElement('div');
modal.className = 'preview-modal';
modal.innerHTML = `
<div class=\\\"modal-content\\\">
<div class=\\\"modal-header\\\">
<h3>Предпросмотр</h3>
<button class=\\\"close-btn\\\">×</button>
</div>
<div class=\\\"modal-body\\\" id=\\\"preview-container\\\"></div>
</div>
`;
document.body.appendChild(modal);
// Отображение контента
renderContentWithoutEditor(content, 'preview-container', {
theme: 'light',
enableMath: true
});
// Закрытие модального окна
modal.querySelector('.close-btn').addEventListener('click', () => {
modal.remove();
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
// Пример 3: Экспорт в PDF/печать
function exportToPDF(content, filename = 'document') {
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${filename}</title>
<meta charset=\\\"UTF-8\\\">
<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>
@media print {
body {
font-family: 'Times New Roman', Times, serif;
line-height: 1.5;
font-size: 12pt;
color: #000;
}
h1, h2, h3, h4 {
page-break-after: avoid;
}
pre, blockquote, table {
page-break-inside: avoid;
}
.katex-display {
page-break-inside: avoid;
}
}
body {
max-width: 210mm;
margin: 20mm auto;
padding: 0 20mm;
}
.katex { font-size: 1em; }
</style>
</head>
<body>
${content.html || convertMarkdownToHTML(content.markdown || content)}
</body>
</html>
`);
printWindow.document.close();
// Автоматическая печать после загрузки
printWindow.onload = function() {
printWindow.print();
};
}
6.3 Оптимизация производительности
// Ленивая загрузка KaTeX и Highlight.js
let katexLoaded = false;
let hljsLoaded = false;
async function loadMathRenderer() {
if (katexLoaded) return;
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js';
script.onload = () => {
katexLoaded = true;
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css';
document.head.appendChild(css);
});
}
async function loadSyntaxHighlighter() {
if (hljsLoaded) return;
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js';
script.onload = () => {
hljsLoaded = true;
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
document.head.appendChild(css);
});
}
// Оптимизированный рендеринг с ленивой загрузкой
async function renderContentOptimized(content, containerId) {
const container = document.getElementById(containerId);
// Показываем спиннер загрузки
container.innerHTML = '<div class=\\\"loading-spinner\\\">Загрузка...</div>';
try {
// Параллельная загрузка зависимостей
await Promise.all([
loadMathRenderer(),
loadSyntaxHighlighter()
]);
// Рендеринг контента
renderContentWithoutEditor(content, containerId, {
enableMath: true,
enableSyntaxHighlight: true
});
} catch (error) {
console.error('Ошибка загрузки зависимостей:', error);
container.innerHTML = `
<div class=\\\"error\\\">
<h3>Ошибка отображения контента</h3>
<p>${error.message}</p>
<button onclick=\\\"renderContentOptimized(content, '${containerId}')\\\">
Повторить попытку
</button>
</div>
`;
}
}
7. Хранение в базе данных
7.1 Структура таблиц для хранения контента
-- Таблица для хранения курсов/статей
CREATE TABLE content (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
description TEXT,
markdown_content TEXT NOT NULL,
html_content TEXT,
metadata JSONB DEFAULT '{}',
author_id UUID REFERENCES users(id),
status VARCHAR(50) DEFAULT 'draft',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
published_at TIMESTAMP WITH TIME ZONE,
version INTEGER DEFAULT 1
);
-- Индексы для оптимизации поиска
CREATE INDEX idx_content_author ON content(author_id);
CREATE INDEX idx_content_status ON content(status);
CREATE INDEX idx_content_created ON content(created_at);
CREATE INDEX idx_content_title ON content(title);
CREATE INDEX idx_content_metadata ON content USING gin(metadata);
-- Таблица для версий контента (история изменений)
CREATE TABLE content_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content_id UUID REFERENCES content(id) ON DELETE CASCADE,
markdown_content TEXT NOT NULL,
html_content TEXT,
version INTEGER NOT NULL,
change_description TEXT,
author_id UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Индекс для быстрого поиска версий
CREATE INDEX idx_content_versions_content ON content_versions(content_id);
CREATE INDEX idx_content_versions_version ON content_versions(version);
-- Таблица для извлеченных формул (для поиска и индексации)
CREATE TABLE content_formulas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content_id UUID REFERENCES content(id) ON DELETE CASCADE,
formula_text TEXT NOT NULL,
formula_type VARCHAR(20), -- 'inline' или 'display'
position INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Индекс для поиска формул
CREATE INDEX idx_formulas_content ON content_formulas(content_id);
CREATE INDEX idx_formulas_text ON content_formulas USING gin(formula_text gin_trgm_ops);
7.2 Модели данных для Node.js/Express
// models/Content.js
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Content = sequelize.define('Content', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
title: {
type: DataTypes.STRING(255),
allowNull: false,
validate: {
notEmpty: true
}
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
markdownContent: {
type: DataTypes.TEXT,
allowNull: false,
field: 'markdown_content'
},
htmlContent: {
type: DataTypes.TEXT,
allowNull: true,
field: 'html_content'
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {},
get() {
const rawValue = this.getDataValue('metadata');
return rawValue ? rawValue : {};
}
},
status: {
type: DataTypes.ENUM('draft', 'published', 'archived'),
defaultValue: 'draft'
},
version: {
type: DataTypes.INTEGER,
defaultValue: 1
},
publishedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'published_at'
}
}, {
tableName: 'content',
timestamps: true,
underscored: true,
hooks: {
beforeSave: async (content) => {
// Автоматическая генерация HTML из Markdown
if (content.changed('markdownContent') && content.markdownContent) {
content.htmlContent = await convertMarkdownToHTML(content.markdownContent);
// Извлечение и сохранение формул
await extractAndSaveFormulas(content);
}
// Обновление версии при изменении контента
if (content.changed('markdownContent')) {
content.version += 1;
}
// Обновление времени публикации
if (content.changed('status') && content.status === 'published') {
content.publishedAt = new Date();
}
},
afterSave: async (content) => {
// Сохранение версии в историю
if (content.changed('markdownContent')) {
await sequelize.models.ContentVersion.create({
contentId: content.id,
markdownContent: content.markdownContent,
htmlContent: content.htmlContent,
version: content.version,
authorId: content.authorId
});
}
}
}
});
Content.associate = (models) => {
Content.belongsTo(models.User, {
foreignKey: 'author_id',
as: 'author'
});
Content.hasMany(models.ContentVersion, {
foreignKey: 'content_id',
as: 'versions'
});
Content.hasMany(models.ContentFormula, {
foreignKey: 'content_id',
as: 'formulas'
});
};
return Content;
};
// models/ContentVersion.js
module.exports = (sequelize) => {
const ContentVersion = sequelize.define('ContentVersion', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
markdownContent: {
type: DataTypes.TEXT,
allowNull: false,
field: 'markdown_content'
},
htmlContent: {
type: DataTypes.TEXT,
allowNull: true,
field: 'html_content'
},
version: {
type: DataTypes.INTEGER,
allowNull: false
},
changeDescription: {
type: DataTypes.TEXT,
allowNull: true,
field: 'change_description'
}
}, {
tableName: 'content_versions',
timestamps: true,
underscored: true
});
ContentVersion.associate = (models) => {
ContentVersion.belongsTo(models.Content, {
foreignKey: 'content_id',
as: 'content'
});
ContentVersion.belongsTo(models.User, {
foreignKey: 'author_id',
as: 'author'
});
};
return ContentVersion;
};
// Вспомогательные функции
async function convertMarkdownToHTML(markdown) {
// Используем Lute или marked.js для конвертации
// с поддержкой KaTeX
// ...
}
async function extractAndSaveFormulas(content) {
const formulas = extractLatexFormulas(content.markdownContent);
// Удаляем старые формулы
await sequelize.models.ContentFormula.destroy({
where: { contentId: content.id }
});
// Сохраняем новые формулы
const formulaPromises = [];
formulas.inline.forEach((formula, index) => {
formulaPromises.push(
sequelize.models.ContentFormula.create({
contentId: content.id,
formulaText: formula.formula,
formulaType: 'inline',
position: formula.position
})
);
});
formulas.display.forEach((formula, index) => {
formulaPromises.push(
sequelize.models.ContentFormula.create({
contentId: content.id,
formulaText: formula.formula,
formulaType: 'display',
position: formula.position
})
);
});
await Promise.all(formulaPromises);
}
7.3 API endpoints для работы с контентом
// routes/content.js
const express = require('express');
const router = express.Router();
const { Content, ContentVersion } = require('../models');
const auth = require('../middleware/auth');
// Получение списка контента
router.get('/', auth, async (req, res) => {
try {
const { page = 1, limit = 20, status, search } = req.query;
const offset = (page - 1) * limit;
const where = {};
if (status) {
where.status = status;
}
if (search) {
where[Op.or] = [
{ title: { [Op.iLike]: `%${search}%` } },
{ description: { [Op.iLike]: `%${search}%` } }
];
}
const { count, rows } = await Content.findAndCountAll({
where,
limit: parseInt(limit),
offset: parseInt(offset),
order: [['created_at', 'DESC']],
include: [{
model: User,
as: 'author',
attributes: ['id', 'name', 'email']
}]
});
res.json({
data: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
pages: Math.ceil(count / limit)
}
});
} catch (error) {
console.error('Ошибка получения контента:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Создание нового контента
router.post('/', auth, async (req, res) => {
try {
const { title, description, markdownContent, metadata } = req.body;
const content = await Content.create({
title,
description,
markdownContent,
metadata: metadata || {},
authorId: req.user.id
});
res.status(201).json({
message: 'Контент создан',
data: content
});
} catch (error) {
console.error('Ошибка создания контента:', error);
res.status(400).json({ error: error.message });
}
});
// Получение контента по ID
router.get('/:id', async (req, res) => {
try {
const content = await Content.findByPk(req.params.id, {
include: [{
model: User,
as: 'author',
attributes: ['id', 'name', 'email']
}]
});
if (!content) {
return res.status(404).json({ error: 'Контент не найден' });
}
res.json({
data: content
});
} catch (error) {
console.error('Ошибка получения контента:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Обновление контента
router.put('/:id', auth, async (req, res) => {
try {
const { title, description, markdownContent, metadata } = req.body;
const content = await Content.findByPk(req.params.id);
if (!content) {
return res.status(404).json({ error: 'Контент не найден' });
}
// Проверка прав доступа
if (content.authorId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Доступ запрещен' });
}
// Обновление полей
await content.update({
title: title || content.title,
description: description !== undefined ? description : content.description,
markdownContent: markdownContent || content.markdownContent,
metadata: metadata || content.metadata
});
res.json({
message: 'Контент обновлен',
data: content
});
} catch (error) {
console.error('Ошибка обновления контента:', error);
res.status(400).json({ error: error.message });
}
});
// Публикация контента
router.post('/:id/publish', auth, async (req, res) => {
try {
const content = await Content.findByPk(req.params.id);
if (!content) {
return res.status(404).json({ error: 'Контент не найден' });
}
if (content.authorId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Доступ запрещен' });
}
await content.update({
status: 'published',
publishedAt: new Date()
});
res.json({
message: 'Контент опубликован',
data: content
});
} catch (error) {
console.error('Ошибка публикации:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Получение версий контента
router.get('/:id/versions', auth, async (req, res) => {
try {
const versions = await ContentVersion.findAll({
where: { contentId: req.params.id },
order: [['version', 'DESC']],
include: [{
model: User,
as: 'author',
attributes: ['id', 'name', 'email']
}]
});
res.json({
data: versions
});
} catch (error) {
console.error('Ошибка получения версий:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Восстановление версии
router.post('/:id/restore/:versionId', auth, async (req, res) => {
try {
const version = await ContentVersion.findByPk(req.params.versionId, {
include: [{
model: Content,
as: 'content',
where: { id: req.params.id }
}]
});
if (!version) {
return res.status(404).json({ error: 'Версия не найдена' });
}
const content = version.content;
if (content.authorId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Доступ запрещен' });
}
// Восстановление контента из версии
await content.update({
markdownContent: version.markdownContent,
htmlContent: version.htmlContent,
version: content.version + 1
});
res.json({
message: 'Версия восстановлена',
data: content
});
} catch (error) {
console.error('Ошибка восстановления:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
module.exports = router;
7.4 Оптимизация хранения больших документов
// Стратегии хранения больших документов
// 1. Компрессия контента
async function compressContent(content) {
// Используем gzip для сжатия больших текстов
const compressed = await new Promise((resolve, reject) => {
zlib.gzip(content, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
return compressed.toString('base64');
}
async function decompressContent(compressed) {
const buffer = Buffer.from(compressed, 'base64');
return new Promise((resolve, reject) => {
zlib.gunzip(buffer, (err, result) => {
if (err) reject(err);
else resolve(result.toString());
});
});
}
// 2. Разделение контента на части
async function splitLargeContent(content, maxChunkSize = 100000) {
if (content.length <= maxChunkSize) {
return [content];
}
const chunks = [];
let currentChunk = '';
const lines = content.split('\\
');
for (const line of lines) {
if ((currentChunk + line + '\\
').length > maxChunkSize) {
chunks.push(currentChunk);
currentChunk = line + '\\
';
} else {
currentChunk += line + '\\
';
}
}
if (currentChunk) {
chunks.push(currentChunk);
}
return chunks;
}
// 3. Ленивая загрузка частей контента
class LazyContentLoader {
constructor(contentId) {
this.contentId = contentId;
this.chunks = [];
this.loadedChunks = new Set();
}
async getChunk(index) {
if (this.loadedChunks.has(index)) {
return this.chunks[index];
}
// Загрузка чанка из базы данных
const response = await fetch(`/api/content/${this.contentId}/chunks/${index}`);
const chunk = await response.json();
this.chunks[index] = chunk;
this.loadedChunks.add(index);
return chunk;
}
async *getContent() {
// Получаем метаданные о количестве чанков
const metadata = await fetch(`/api/content/${this.contentId}/metadata`);
const { totalChunks } = await metadata.json();
for (let i = 0; i < totalChunks; i++) {
yield await this.getChunk(i);
}
}
}
// 4. Кэширование в Redis
const redis = require('redis');
const client = redis.createClient();
async function getContentWithCache(contentId) {
const cacheKey = `content:${contentId}`;
// Попытка получить из кэша
const cached = await client.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Загрузка из базы данных
const content = await Content.findByPk(contentId);
if (!content) {
return null;
}
// Сохранение в кэш на 1 час
await client.setex(cacheKey, 3600, JSON.stringify(content));
return content;
}
// 5. Индексация для полнотекстового поиска
async function indexContentForSearch(content) {
// Извлечение текста без Markdown разметки
const plainText = content.markdownContent
.replace(/#+\\\\s*/g, '') // Удаляем заголовки
.replace(/\\\\*\\\\*?(.*?)\\\\*\\\\*?/g, '$1') // Удаляем жирный/курсив
.replace(/`(.*?)`/g, '$1') // Удаляем inline код
.replace(/\\\\[(.*?)\\\\]\\\\(.*?\\\\)/g, '$1') // Удаляем ссылки
.replace(/\\\\$\\\\$?(.*?)\\\\$?\\\\$/g, '') // Удаляем формулы
.replace(/```[\\\\s\\\\S]*?```/g, '') // Удаляем блоки кода
.replace(/[^\\\\w\\\\sа-яА-ЯёЁ]/g, ' ') // Оставляем только буквы и пробелы
.replace(/\\\\s+/g, ' ') // Убираем лишние пробелы
.trim();
// Сохранение в поисковый индекс (Elasticsearch или PostgreSQL tsvector)
await saveToSearchIndex(content.id, plainText, {
title: content.title,
description: content.description,
author: content.authorId,
status: content.status,
publishedAt: content.publishedAt
});
}
8. Примеры использования
8.1 Полный пример: Система управления курсами
// course-system.js
class CourseSystem {
constructor() {
this.vditorInstance = null;
this.currentCourse = null;
this.draftManager = null;
}
// Инициализация системы
async initialize() {
// Загрузка Vditor
await this.loadVditor();
// Инициализация менеджера черновиков
this.draftManager = new DraftManager(this.vditorInstance, 'course_draft');
this.draftManager.enableAutoSave(30000); // Автосохранение каждые 30 секунд
// Настройка обработчиков событий
this.setupEventHandlers();
// Загрузка сохраненного черновика
const draft = this.draftManager.loadDraft();
if (draft) {
this.showDraftNotification(draft);
}
console.log('Система управления курсами инициализирована');
}
// Загрузка Vditor
async loadVditor() {
return new Promise((resolve, reject) => {
if (typeof Vditor !== 'undefined') {
this.initVditorEditor();
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://unpkg.com/vditor@3.10.4/dist/index.min.js';
script.onload = () => {
this.initVditorEditor();
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
});
}
// Инициализация редактора
initVditorEditor() {
this.vditorInstance = new Vditor('vditor-container', {
height: 600,
placeholder: 'Начните создавать курс...',
theme: 'classic',
lang: 'ru_RU',
toolbar: [
'emoji', 'headings', 'bold', 'italic',.