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

37 KiB
Raw Blame History

Руководство по работе с 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\\\">&times;</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',.