diff --git a/index.html b/index.html
index 5de9ecd..59a77d7 100644
--- a/index.html
+++ b/index.html
@@ -697,6 +697,74 @@
const yamlStatus = document.getElementById('yamlStatus');
let currentYaml = '';
+ // Docker Compose structure validation
+ function validateDockerCompose(config) {
+ const errors = [];
+
+ // Check for required top-level keys
+ if (!config.services) {
+ errors.push('Missing "services" section');
+ return errors;
+ }
+
+ // Check services
+ const serviceNames = Object.keys(config.services);
+ if (serviceNames.length === 0) {
+ errors.push('No services defined');
+ }
+
+ // Validate each service
+ serviceNames.forEach(serviceName => {
+ const service = config.services[serviceName];
+
+ // Check for required service fields
+ if (!service.image && !service.build) {
+ errors.push(`Service "${serviceName}": Missing "image" or "build"`);
+ }
+
+ // Check volume references match defined volumes
+ if (service.volumes && Array.isArray(service.volumes)) {
+ service.volumes.forEach(volume => {
+ // Extract volume name from volume string (format: "volume_name:/path" or "/host:/path")
+ if (typeof volume === 'string') {
+ const parts = volume.split(':');
+ const volumeName = parts[0];
+
+ // Check if it's a named volume (not a bind mount starting with / or ./)
+ if (volumeName && !volumeName.startsWith('/') && !volumeName.startsWith('./') && volumeName !== '~') {
+ // Check if volume is defined
+ if (config.volumes && !config.volumes.hasOwnProperty(volumeName)) {
+ errors.push(`Service "${serviceName}": Volume "${volumeName}" is referenced but not defined in "volumes" section`);
+ }
+ }
+ }
+ });
+ }
+
+ // Validate port format
+ if (service.ports && Array.isArray(service.ports)) {
+ service.ports.forEach(port => {
+ if (typeof port === 'string') {
+ // Basic port format check: "host:container" or just number
+ const parts = port.split(':');
+ if (parts.length === 2 && (!parts[0].match(/^\d+$/) || !parts[1].match(/^\d+\/?\w*$/))) {
+ errors.push(`Service "${serviceName}": Invalid port format "${port}" (expected "host:container")`);
+ }
+ }
+ });
+ }
+
+ // Check environment variables format
+ if (service.environment) {
+ if (typeof service.environment !== 'object') {
+ errors.push(`Service "${serviceName}": "environment" must be an object or array`);
+ }
+ }
+ });
+
+ return errors;
+ }
+
// YAML validation function
function validateYAML(yamlText) {
yamlError.classList.remove('show');
@@ -706,27 +774,56 @@
yamlStatus.classList.remove('valid', 'invalid');
if (!yamlText || yamlText.trim() === '') {
+ yamlStatus.textContent = 'YAML is empty';
+ yamlStatus.classList.add('invalid');
return { valid: false, error: 'YAML is empty' };
}
+ let parsedConfig;
+ let yamlError = null;
+
+ // First, check YAML syntax
try {
- jsyaml.load(yamlText);
- yamlEditor.classList.add('valid');
- yamlError.classList.remove('show');
- yamlSuccess.classList.add('show');
- yamlStatus.textContent = '✓ Valid YAML';
- yamlStatus.classList.add('valid');
- return { valid: true };
+ parsedConfig = jsyaml.load(yamlText);
} catch (error) {
yamlEditor.classList.add('error');
yamlEditor.classList.remove('valid');
- yamlError.textContent = `YAML Error: ${error.message}`;
- yamlError.classList.add('show');
- yamlSuccess.classList.remove('show');
- yamlStatus.textContent = '✗ Invalid YAML';
- yamlStatus.classList.add('invalid');
- return { valid: false, error: error.message };
+ yamlError = error;
}
+
+ if (yamlError) {
+ // YAML syntax error
+ document.getElementById('yamlError').textContent = `YAML Syntax Error: ${yamlError.message}`;
+ document.getElementById('yamlError').classList.add('show');
+ yamlSuccess.classList.remove('show');
+ yamlStatus.textContent = '✗ Invalid YAML Syntax';
+ yamlStatus.classList.add('invalid');
+ return { valid: false, error: yamlError.message };
+ }
+
+ // YAML is syntactically valid, now check Docker Compose structure
+ const composeErrors = validateDockerCompose(parsedConfig);
+
+ if (composeErrors.length > 0) {
+ // Docker Compose structure errors
+ yamlEditor.classList.add('error');
+ yamlEditor.classList.remove('valid');
+ document.getElementById('yamlError').textContent = `Docker Compose Errors:\n• ${composeErrors.join('\n• ')}`;
+ document.getElementById('yamlError').classList.add('show');
+ yamlSuccess.classList.remove('show');
+ yamlStatus.textContent = '✗ Docker Compose Validation Failed';
+ yamlStatus.classList.add('invalid');
+ return { valid: false, error: composeErrors.join('; ') };
+ }
+
+ // All valid!
+ yamlEditor.classList.add('valid');
+ yamlEditor.classList.remove('error');
+ document.getElementById('yamlError').classList.remove('show');
+ yamlSuccess.classList.add('show');
+ yamlStatus.textContent = '✓ Valid Docker Compose YAML';
+ yamlStatus.classList.add('valid');
+ return { valid: true };
}
// Real-time YAML linting with debounce