/** * Редактор кода с подсветкой синтаксиса * Использует CodeMirror 6 * Автор: Сергей Антропов * Сайт: https://devops.org.ru */ // Инициализация CodeMirror редактора function initCodeEditor(textareaId, language = 'yaml', options = {}) { const textarea = document.getElementById(textareaId); if (!textarea) { console.error(`Textarea with id "${textareaId}" not found`); return null; } // Проверяем, что CodeMirror загружен if (typeof CodeMirror === 'undefined') { console.error('CodeMirror is not loaded. Please include CodeMirror library.'); return null; } const defaultOptions = { lineNumbers: true, mode: language, theme: 'default', indentUnit: 2, indentWithTabs: false, lineWrapping: true, matchBrackets: true, autoCloseBrackets: true, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], ...options }; const editor = CodeMirror.fromTextArea(textarea, defaultOptions); // Сохраняем значение обратно в textarea при изменении editor.on('change', function(cm) { cm.save(); }); return editor; } // Валидация YAML function validateYAML(content) { try { // Используем js-yaml если доступен, иначе простую проверку if (typeof jsyaml !== 'undefined') { jsyaml.load(content); return { valid: true, errors: [] }; } else { // Простая проверка синтаксиса const lines = content.split('\n'); const errors = []; let indentStack = [0]; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); // Пропускаем пустые строки и комментарии if (!trimmed || trimmed.startsWith('#')) { continue; } // Проверка отступов const indent = line.length - line.trimStart().length; const lastIndent = indentStack[indentStack.length - 1]; if (indent > lastIndent + 2) { errors.push({ line: i + 1, message: `Неправильный отступ на строке ${i + 1}` }); } // Проверка ключей без значений if (trimmed.endsWith(':') && !trimmed.includes('{') && !trimmed.includes('[')) { // Это нормально для YAML } } return { valid: errors.length === 0, errors: errors }; } } catch (e) { return { valid: false, errors: [{ line: 1, message: e.message }] }; } } // Валидация Ansible playbook function validateAnsiblePlaybook(content) { const yamlValidation = validateYAML(content); if (!yamlValidation.valid) { return yamlValidation; } try { if (typeof jsyaml !== 'undefined') { const parsed = jsyaml.load(content); if (!Array.isArray(parsed)) { return { valid: false, errors: [{ line: 1, message: 'Playbook должен быть списком (массивом)' }] }; } const errors = []; parsed.forEach((play, index) => { if (!play.hosts && !play.tasks && !play.roles) { errors.push({ line: index + 1, message: `Play ${index + 1}: должен содержать hosts, tasks или roles` }); } }); return { valid: errors.length === 0, errors: errors }; } } catch (e) { return { valid: false, errors: [{ line: 1, message: e.message }] }; } return { valid: true, errors: [] }; } // Показ ошибок валидации в редакторе function showValidationErrors(editor, errors) { if (!editor || !editor._validationMarkers) { return; } // Очищаем предыдущие маркеры editor._validationMarkers.forEach(marker => { if (marker && typeof marker.clear === 'function') { marker.clear(); } else if (marker && typeof marker.remove === 'function') { marker.remove(); } }); editor._validationMarkers = []; // Очищаем все gutter маркеры editor.clearGutter('CodeMirror-linenumbers'); errors.forEach(error => { const line = Math.max(0, (error.line || 1) - 1); // CodeMirror использует 0-based индексы try { // Добавляем класс для подсветки строки с ошибкой editor.addLineClass(line, 'background', 'cm-error-line'); // Добавляем маркер в gutter const marker = document.createElement('span'); marker.className = 'cm-error-marker'; marker.textContent = '⚠'; marker.title = error.message || 'Ошибка'; editor.setGutterMarker(line, 'CodeMirror-linenumbers', marker); editor._validationMarkers.push({ line: line, marker: marker }); } catch (e) { console.warn('Error adding validation marker:', e); } }); } // Экспорт функций window.CodeEditor = { init: initCodeEditor, validateYAML: validateYAML, validateAnsible: validateAnsiblePlaybook, showErrors: showValidationErrors };