Markdown Form Validation and Input Handling: Complete Guide for Interactive Documentation with Dynamic Validation Systems
Advanced Markdown form validation and input handling systems enable interactive documentation with robust data validation, real-time user feedback, and comprehensive error handling mechanisms. By implementing sophisticated validation frameworks, dynamic input processing systems, and user-friendly feedback interfaces, technical teams can create documentation that not only informs but also collects, validates, and processes user input effectively while maintaining professional standards for data quality and user experience.
Why Master Form Validation and Input Handling?
Professional form validation provides essential benefits for interactive documentation:
- Data Quality: Ensure user-submitted data meets requirements through comprehensive validation rules
- User Experience: Provide immediate feedback and guidance to help users complete forms successfully
- Security: Protect against malicious input and maintain data integrity through proper validation
- Automation: Enable automated processing of validated user input for dynamic content generation
- Accessibility: Create inclusive form experiences that work across different assistive technologies
Foundation Validation Principles
Comprehensive Input Validation Framework
Building robust validation systems that handle multiple input types and validation scenarios:
// form-validator.js - Advanced form validation system for Markdown documentation
class MarkdownFormValidator {
constructor(options = {}) {
this.options = {
// Validation configuration
validateOnInput: true,
validateOnBlur: true,
showErrorsInline: true,
highlightInvalidFields: true,
// Error display options
errorClass: 'validation-error',
successClass: 'validation-success',
errorSummaryId: 'validation-summary',
// Validation timing
debounceDelay: 300,
asyncValidationDelay: 500,
// Custom validation rules
customValidators: new Map(),
// Internationalization
locale: 'en',
messages: {},
...options
};
this.validationRules = new Map();
this.validationResults = new Map();
this.validationQueue = new Map();
this.validatedForms = new Set();
this.setupDefaultValidators();
this.init();
}
init() {
this.loadValidationMessages();
this.setupFormObserver();
this.setupEventListeners();
console.log('Markdown Form Validator initialized');
}
setupDefaultValidators() {
// Basic validators
this.addValidator('required', this.validateRequired.bind(this));
this.addValidator('minLength', this.validateMinLength.bind(this));
this.addValidator('maxLength', this.validateMaxLength.bind(this));
this.addValidator('pattern', this.validatePattern.bind(this));
this.addValidator('email', this.validateEmail.bind(this));
this.addValidator('url', this.validateUrl.bind(this));
this.addValidator('number', this.validateNumber.bind(this));
// Advanced validators
this.addValidator('custom', this.validateCustom.bind(this));
this.addValidator('async', this.validateAsync.bind(this));
this.addValidator('conditional', this.validateConditional.bind(this));
this.addValidator('crossField', this.validateCrossField.bind(this));
// Content-specific validators
this.addValidator('markdown', this.validateMarkdown.bind(this));
this.addValidator('codeBlock', this.validateCodeBlock.bind(this));
this.addValidator('frontmatter', this.validateFrontmatter.bind(this));
}
addValidator(name, validatorFunction) {
this.options.customValidators.set(name, validatorFunction);
}
loadValidationMessages() {
const defaultMessages = {
required: 'This field is required',
minLength: 'Must be at least {min} characters long',
maxLength: 'Must not exceed {max} characters',
pattern: 'Please enter a valid value',
email: 'Please enter a valid email address',
url: 'Please enter a valid URL',
number: 'Please enter a valid number',
markdown: 'Invalid Markdown syntax',
codeBlock: 'Code block contains syntax errors',
frontmatter: 'Invalid YAML frontmatter',
async: 'Validation failed',
conditional: 'Field validation failed',
crossField: 'Fields do not match'
};
this.options.messages = { ...defaultMessages, ...this.options.messages };
}
setupFormObserver() {
// Observe DOM for new forms
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const forms = node.querySelectorAll ?
node.querySelectorAll('form[data-validate]') :
[];
forms.forEach(form => this.initializeForm(form));
// Check if the node itself is a form
if (node.matches && node.matches('form[data-validate]')) {
this.initializeForm(node);
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Initialize existing forms
document.querySelectorAll('form[data-validate]').forEach(form => {
this.initializeForm(form);
});
}
initializeForm(form) {
if (this.validatedForms.has(form)) return;
this.validatedForms.add(form);
const formId = form.id || `form-${Date.now()}`;
form.id = formId;
// Parse validation rules from data attributes
this.parseFormValidationRules(form);
// Setup form-level validation
form.addEventListener('submit', (event) => {
this.handleFormSubmit(event);
});
// Setup field-level validation
const fields = form.querySelectorAll('[data-validate]');
fields.forEach(field => {
this.initializeField(field, form);
});
console.log(`Initialized form validation for ${formId}`);
}
initializeField(field, form) {
const fieldName = field.name || field.id || `field-${Date.now()}`;
// Parse field validation rules
this.parseFieldValidationRules(field, form.id);
// Setup event listeners
if (this.options.validateOnInput) {
const inputHandler = this.debounce(
() => this.validateField(field, form),
this.options.debounceDelay
);
field.addEventListener('input', inputHandler);
field.addEventListener('keyup', inputHandler);
}
if (this.options.validateOnBlur) {
field.addEventListener('blur', () => {
this.validateField(field, form);
});
}
// Special handling for different input types
this.setupSpecialFieldHandling(field, form);
}
setupSpecialFieldHandling(field, form) {
const fieldType = field.type || field.tagName.toLowerCase();
switch (fieldType) {
case 'file':
field.addEventListener('change', () => {
this.validateFileField(field, form);
});
break;
case 'checkbox':
case 'radio':
field.addEventListener('change', () => {
this.validateField(field, form);
});
break;
case 'select':
case 'select-one':
case 'select-multiple':
field.addEventListener('change', () => {
this.validateField(field, form);
});
break;
}
// Special handling for Markdown editors
if (field.classList.contains('markdown-editor')) {
this.setupMarkdownEditorValidation(field, form);
}
}
setupMarkdownEditorValidation(field, form) {
// Handle various Markdown editor implementations
const editorType = field.getAttribute('data-editor-type');
switch (editorType) {
case 'codemirror':
this.setupCodeMirrorValidation(field, form);
break;
case 'ace':
this.setupAceEditorValidation(field, form);
break;
case 'simple':
default:
// Standard textarea handling
break;
}
}
parseFormValidationRules(form) {
const formId = form.id;
const rules = {
fields: new Map(),
crossFieldRules: [],
customRules: []
};
// Parse form-level validation rules
const formRules = form.getAttribute('data-validate');
if (formRules) {
try {
const parsed = JSON.parse(formRules);
rules.crossFieldRules = parsed.crossField || [];
rules.customRules = parsed.custom || [];
} catch (error) {
console.warn('Failed to parse form validation rules:', error);
}
}
this.validationRules.set(formId, rules);
}
parseFieldValidationRules(field, formId) {
const fieldName = field.name || field.id;
const rules = [];
// Parse validation rules from data attributes
const validationData = field.getAttribute('data-validate');
if (validationData) {
try {
const parsed = JSON.parse(validationData);
if (Array.isArray(parsed)) {
rules.push(...parsed);
} else {
rules.push(parsed);
}
} catch (error) {
// Try parsing as simple rule list
const simpleRules = validationData.split('|');
rules.push(...simpleRules.map(rule => {
const [name, ...params] = rule.split(':');
return {
rule: name.trim(),
params: params.length > 0 ? params.join(':') : undefined
};
}));
}
}
// Add HTML5 validation attributes
this.addHtml5ValidationRules(field, rules);
// Store rules
const formRules = this.validationRules.get(formId);
formRules.fields.set(fieldName, rules);
}
addHtml5ValidationRules(field, rules) {
// Required attribute
if (field.hasAttribute('required')) {
rules.unshift({ rule: 'required' });
}
// Min/max length
if (field.hasAttribute('minlength')) {
rules.push({
rule: 'minLength',
params: field.getAttribute('minlength')
});
}
if (field.hasAttribute('maxlength')) {
rules.push({
rule: 'maxLength',
params: field.getAttribute('maxlength')
});
}
// Pattern
if (field.hasAttribute('pattern')) {
rules.push({
rule: 'pattern',
params: field.getAttribute('pattern')
});
}
// Type-specific validations
switch (field.type) {
case 'email':
rules.push({ rule: 'email' });
break;
case 'url':
rules.push({ rule: 'url' });
break;
case 'number':
rules.push({ rule: 'number' });
break;
}
}
async validateField(field, form) {
const formId = form.id;
const fieldName = field.name || field.id;
const formRules = this.validationRules.get(formId);
const fieldRules = formRules?.fields.get(fieldName) || [];
const validationResult = {
valid: true,
errors: [],
warnings: [],
field: fieldName,
value: this.getFieldValue(field)
};
// Run validation rules
for (const ruleConfig of fieldRules) {
const ruleName = ruleConfig.rule;
const params = ruleConfig.params;
const validator = this.options.customValidators.get(ruleName);
if (validator) {
try {
const result = await validator(validationResult.value, params, field, form);
if (!result.valid) {
validationResult.valid = false;
validationResult.errors.push({
rule: ruleName,
message: this.formatErrorMessage(ruleName, params, result.message),
params
});
// Stop on first error if configured
if (ruleConfig.stopOnError !== false) {
break;
}
}
if (result.warnings) {
validationResult.warnings.push(...result.warnings);
}
} catch (error) {
console.error(`Validation error for rule ${ruleName}:`, error);
validationResult.valid = false;
validationResult.errors.push({
rule: ruleName,
message: 'Validation failed due to internal error',
params
});
}
}
}
// Store validation result
this.validationResults.set(`${formId}.${fieldName}`, validationResult);
// Update UI
this.updateFieldValidationUI(field, validationResult);
return validationResult;
}
getFieldValue(field) {
switch (field.type) {
case 'checkbox':
return field.checked;
case 'radio':
const form = field.closest('form');
const radioGroup = form.querySelectorAll(`input[name="${field.name}"]`);
for (const radio of radioGroup) {
if (radio.checked) {
return radio.value;
}
}
return null;
case 'file':
return field.files;
case 'select-multiple':
return Array.from(field.selectedOptions).map(option => option.value);
default:
return field.value;
}
}
// Validation rule implementations
validateRequired(value, params, field) {
const isEmpty = value === null || value === undefined ||
(typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0) ||
(field.type === 'checkbox' && !value);
return {
valid: !isEmpty,
message: isEmpty ? this.options.messages.required : null
};
}
validateMinLength(value, params, field) {
const minLength = parseInt(params);
const length = typeof value === 'string' ? value.length : 0;
const valid = length >= minLength;
return {
valid,
message: valid ? null : this.options.messages.minLength.replace('{min}', minLength)
};
}
validateMaxLength(value, params, field) {
const maxLength = parseInt(params);
const length = typeof value === 'string' ? value.length : 0;
const valid = length <= maxLength;
return {
valid,
message: valid ? null : this.options.messages.maxLength.replace('{max}', maxLength)
};
}
validatePattern(value, params, field) {
if (!value || typeof value !== 'string') {
return { valid: true }; // Pattern validation only applies to non-empty strings
}
const pattern = new RegExp(params);
const valid = pattern.test(value);
return {
valid,
message: valid ? null : this.options.messages.pattern
};
}
validateEmail(value, params, field) {
if (!value || typeof value !== 'string') {
return { valid: true };
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const valid = emailPattern.test(value);
return {
valid,
message: valid ? null : this.options.messages.email
};
}
validateUrl(value, params, field) {
if (!value || typeof value !== 'string') {
return { valid: true };
}
try {
new URL(value);
return { valid: true };
} catch {
return {
valid: false,
message: this.options.messages.url
};
}
}
validateNumber(value, params, field) {
if (!value || value === '') {
return { valid: true };
}
const num = parseFloat(value);
const valid = !isNaN(num) && isFinite(num);
return {
valid,
message: valid ? null : this.options.messages.number
};
}
validateMarkdown(value, params, field) {
if (!value || typeof value !== 'string') {
return { valid: true };
}
const errors = [];
const warnings = [];
// Check for common Markdown syntax issues
this.validateMarkdownSyntax(value, errors, warnings);
return {
valid: errors.length === 0,
message: errors.length > 0 ? errors[0] : null,
warnings: warnings
};
}
validateMarkdownSyntax(content, errors, warnings) {
// Check for unmatched brackets
const openBrackets = (content.match(/\[/g) || []).length;
const closeBrackets = (content.match(/\]/g) || []).length;
if (openBrackets !== closeBrackets) {
errors.push('Unmatched square brackets in Markdown links');
}
// Check for unmatched parentheses in links
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
let match;
while ((match = linkPattern.exec(content)) !== null) {
const linkUrl = match[2];
if (!linkUrl.trim()) {
warnings.push(`Empty URL in link: "${match[1]}"`);
}
}
// Check for code block consistency
const codeBlockMatches = content.match(/```/g) || [];
if (codeBlockMatches.length % 2 !== 0) {
errors.push('Unmatched code block delimiters (```)');
}
// Check for heading structure
const headings = content.match(/^#+\s+.+$/gm) || [];
for (const heading of headings) {
const level = heading.match(/^#+/)[0].length;
if (level > 6) {
warnings.push(`Heading level ${level} exceeds maximum (6)`);
}
}
}
validateCodeBlock(value, params, field) {
if (!value || typeof value !== 'string') {
return { valid: true };
}
const language = params || 'javascript';
const errors = [];
try {
// Basic syntax validation based on language
switch (language.toLowerCase()) {
case 'json':
JSON.parse(value);
break;
case 'javascript':
case 'js':
// Basic JS validation - check for common syntax issues
this.validateJavaScriptSyntax(value, errors);
break;
case 'yaml':
case 'yml':
// Basic YAML validation
this.validateYAMLSyntax(value, errors);
break;
}
} catch (error) {
errors.push(`${language} syntax error: ${error.message}`);
}
return {
valid: errors.length === 0,
message: errors.length > 0 ? errors[0] : null
};
}
validateJavaScriptSyntax(code, errors) {
// Basic bracket matching
const brackets = { '(': ')', '[': ']', '{': '}' };
const stack = [];
for (let i = 0; i < code.length; i++) {
const char = code[i];
if (brackets[char]) {
stack.push(char);
} else if (Object.values(brackets).includes(char)) {
const last = stack.pop();
if (!last || brackets[last] !== char) {
errors.push(`Mismatched bracket at position ${i}`);
break;
}
}
}
if (stack.length > 0) {
errors.push('Unclosed brackets');
}
}
validateYAMLSyntax(yaml, errors) {
const lines = yaml.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for tabs (YAML should use spaces)
if (line.includes('\t')) {
errors.push(`Line ${i + 1}: YAML should use spaces, not tabs`);
}
// Check for basic key-value format
if (line.trim() && !line.trim().startsWith('#') &&
!line.includes(':') && !line.trim().startsWith('-')) {
errors.push(`Line ${i + 1}: Invalid YAML syntax`);
}
}
}
async validateAsync(value, params, field, form) {
// Example async validation (e.g., check if username is available)
try {
const config = JSON.parse(params);
const response = await fetch(config.url, {
method: config.method || 'POST',
headers: {
'Content-Type': 'application/json',
...config.headers
},
body: JSON.stringify({ value, field: field.name })
});
const result = await response.json();
return {
valid: result.valid,
message: result.message || this.options.messages.async
};
} catch (error) {
return {
valid: false,
message: 'Validation service unavailable'
};
}
}
validateConditional(value, params, field, form) {
try {
const condition = JSON.parse(params);
const targetField = form.querySelector(`[name="${condition.field}"]`);
if (!targetField) {
return { valid: true }; // Skip if target field not found
}
const targetValue = this.getFieldValue(targetField);
const shouldValidate = this.evaluateCondition(targetValue, condition);
if (!shouldValidate) {
return { valid: true }; // Skip validation
}
// Run nested validation
const nestedRule = condition.rule;
const nestedParams = condition.params;
const validator = this.options.customValidators.get(nestedRule);
if (validator) {
return validator(value, nestedParams, field, form);
}
return { valid: true };
} catch (error) {
return {
valid: false,
message: 'Conditional validation error'
};
}
}
evaluateCondition(value, condition) {
switch (condition.operator) {
case 'equals':
return value === condition.value;
case 'not_equals':
return value !== condition.value;
case 'greater_than':
return parseFloat(value) > parseFloat(condition.value);
case 'less_than':
return parseFloat(value) < parseFloat(condition.value);
case 'contains':
return typeof value === 'string' && value.includes(condition.value);
case 'not_empty':
return value !== null && value !== undefined && value !== '';
default:
return false;
}
}
updateFieldValidationUI(field, validationResult) {
// Remove existing validation classes
field.classList.remove(this.options.errorClass, this.options.successClass);
// Remove existing error messages
const existingErrors = field.parentNode.querySelectorAll('.field-error');
existingErrors.forEach(error => error.remove());
if (validationResult.valid) {
field.classList.add(this.options.successClass);
} else {
field.classList.add(this.options.errorClass);
if (this.options.showErrorsInline) {
this.displayFieldErrors(field, validationResult.errors);
}
}
// Display warnings
if (validationResult.warnings.length > 0) {
this.displayFieldWarnings(field, validationResult.warnings);
}
// Update global validation summary
this.updateValidationSummary();
}
displayFieldErrors(field, errors) {
const errorContainer = document.createElement('div');
errorContainer.className = 'field-error';
errors.forEach(error => {
const errorElement = document.createElement('span');
errorElement.className = 'error-message';
errorElement.textContent = error.message;
errorElement.setAttribute('role', 'alert');
errorContainer.appendChild(errorElement);
});
field.parentNode.insertBefore(errorContainer, field.nextSibling);
}
displayFieldWarnings(field, warnings) {
const warningContainer = document.createElement('div');
warningContainer.className = 'field-warning';
warnings.forEach(warning => {
const warningElement = document.createElement('span');
warningElement.className = 'warning-message';
warningElement.textContent = warning;
warningContainer.appendChild(warningElement);
});
field.parentNode.insertBefore(warningContainer, field.nextSibling);
}
async handleFormSubmit(event) {
event.preventDefault();
const form = event.target;
const formId = form.id;
// Validate all fields
const fields = form.querySelectorAll('[data-validate]');
const validationPromises = Array.from(fields).map(field =>
this.validateField(field, form)
);
const results = await Promise.all(validationPromises);
const hasErrors = results.some(result => !result.valid);
// Run cross-field validation
const crossFieldResults = await this.validateCrossFieldRules(form);
const hasCrossFieldErrors = crossFieldResults.some(result => !result.valid);
if (hasErrors || hasCrossFieldErrors) {
this.displayValidationSummary(form, [...results, ...crossFieldResults]);
return false;
}
// Form is valid - proceed with submission
this.handleValidFormSubmission(form);
return true;
}
async validateCrossFieldRules(form) {
const formId = form.id;
const formRules = this.validationRules.get(formId);
const crossFieldRules = formRules?.crossFieldRules || [];
const results = [];
for (const rule of crossFieldRules) {
const result = await this.validateCrossFieldRule(form, rule);
results.push(result);
}
return results;
}
async validateCrossFieldRule(form, rule) {
switch (rule.type) {
case 'match':
return this.validateFieldsMatch(form, rule);
case 'password_confirmation':
return this.validatePasswordConfirmation(form, rule);
case 'date_range':
return this.validateDateRange(form, rule);
default:
return { valid: true };
}
}
validateFieldsMatch(form, rule) {
const field1 = form.querySelector(`[name="${rule.field1}"]`);
const field2 = form.querySelector(`[name="${rule.field2}"]`);
if (!field1 || !field2) {
return { valid: true }; // Skip if fields not found
}
const value1 = this.getFieldValue(field1);
const value2 = this.getFieldValue(field2);
const valid = value1 === value2;
return {
valid,
field: rule.field2,
message: valid ? null : rule.message || this.options.messages.crossField
};
}
handleValidFormSubmission(form) {
// Collect form data
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// Trigger custom submission handler if defined
const submitHandler = form.getAttribute('data-submit-handler');
if (submitHandler && window[submitHandler]) {
window[submitHandler](data, form);
return;
}
// Default submission handling
this.submitFormData(form, data);
}
async submitFormData(form, data) {
const submitUrl = form.action || '#';
const method = form.method || 'POST';
try {
const response = await fetch(submitUrl, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
this.displaySubmissionSuccess(form);
} else {
this.displaySubmissionError(form, 'Submission failed');
}
} catch (error) {
this.displaySubmissionError(form, error.message);
}
}
displaySubmissionSuccess(form) {
const successMessage = document.createElement('div');
successMessage.className = 'submission-success';
successMessage.textContent = 'Form submitted successfully!';
successMessage.setAttribute('role', 'alert');
form.insertBefore(successMessage, form.firstChild);
setTimeout(() => {
successMessage.remove();
}, 5000);
}
displaySubmissionError(form, message) {
const errorMessage = document.createElement('div');
errorMessage.className = 'submission-error';
errorMessage.textContent = `Submission error: ${message}`;
errorMessage.setAttribute('role', 'alert');
form.insertBefore(errorMessage, form.firstChild);
}
formatErrorMessage(ruleName, params, customMessage) {
if (customMessage) return customMessage;
let message = this.options.messages[ruleName] || 'Validation failed';
if (params && typeof params === 'string') {
message = message.replace(/\{(\w+)\}/g, (match, key) => {
return params[key] || match;
});
}
return message;
}
debounce(func, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Public API methods
validateForm(formId) {
const form = document.getElementById(formId);
if (form) {
const submitEvent = new Event('submit', { cancelable: true });
return this.handleFormSubmit(submitEvent);
}
return false;
}
getFormValidationResults(formId) {
const results = new Map();
for (const [key, result] of this.validationResults) {
if (key.startsWith(`${formId}.`)) {
const fieldName = key.substring(formId.length + 1);
results.set(fieldName, result);
}
}
return results;
}
clearValidationResults(formId) {
const keysToRemove = [];
for (const key of this.validationResults.keys()) {
if (key.startsWith(`${formId}.`)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => this.validationResults.delete(key));
// Clear UI
const form = document.getElementById(formId);
if (form) {
const fields = form.querySelectorAll('[data-validate]');
fields.forEach(field => {
field.classList.remove(this.options.errorClass, this.options.successClass);
const errors = field.parentNode.querySelectorAll('.field-error, .field-warning');
errors.forEach(error => error.remove());
});
}
}
}
// Usage example and initialization
document.addEventListener('DOMContentLoaded', () => {
const validator = new MarkdownFormValidator({
validateOnInput: true,
validateOnBlur: true,
debounceDelay: 300,
showErrorsInline: true,
// Custom validation messages
messages: {
required: 'This field is required for processing',
email: 'Please provide a valid email address',
markdown: 'Invalid Markdown syntax detected'
}
});
// Add custom validators
validator.addValidator('githubUsername', (value) => {
if (!value) return { valid: true };
const githubPattern = /^[a-zA-Z0-9]([a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
const valid = githubPattern.test(value);
return {
valid,
message: valid ? null : 'Please enter a valid GitHub username'
};
});
validator.addValidator('markdownHeadings', (value) => {
if (!value) return { valid: true };
const headings = value.match(/^#+\s+.+$/gm) || [];
const errors = [];
// Check heading hierarchy
let lastLevel = 0;
for (const heading of headings) {
const level = heading.match(/^#+/)[0].length;
if (level > lastLevel + 1) {
errors.push(`Heading level ${level} skips levels`);
break;
}
lastLevel = level;
}
return {
valid: errors.length === 0,
message: errors.length > 0 ? errors[0] : null
};
});
window.markdownFormValidator = validator;
});
Dynamic Input Processing System
Implementing real-time input processing and validation feedback:
<!-- interactive-form-example.html - Example Markdown documentation form -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Markdown Documentation Form</title>
<link rel="stylesheet" href="form-validation-styles.css">
</head>
<body>
<!-- Example: Markdown Content Submission Form -->
<form id="markdown-content-form" data-validate='{"crossField":[{"type":"match","field1":"email","field2":"email_confirm","message":"Email addresses must match"}]}' class="markdown-form">
<h2>Submit Markdown Content</h2>
<div class="form-group">
<label for="author-name">Author Name *</label>
<input type="text"
id="author-name"
name="author"
data-validate='[{"rule":"required"},{"rule":"minLength","params":"2"}]'
aria-describedby="author-help"
class="form-control">
<div id="author-help" class="form-help">
Enter your full name as it should appear in the documentation.
</div>
</div>
<div class="form-group">
<label for="author-email">Email Address *</label>
<input type="email"
id="author-email"
name="email"
data-validate='[{"rule":"required"},{"rule":"email"}]'
class="form-control">
</div>
<div class="form-group">
<label for="email-confirm">Confirm Email *</label>
<input type="email"
id="email-confirm"
name="email_confirm"
data-validate='[{"rule":"required"}]'
class="form-control">
</div>
<div class="form-group">
<label for="github-username">GitHub Username</label>
<input type="text"
id="github-username"
name="github"
data-validate='[{"rule":"githubUsername"}]'
class="form-control"
placeholder="your-username">
<div class="form-help">
Optional: Link your GitHub profile for attribution.
</div>
</div>
<div class="form-group">
<label for="content-title">Content Title *</label>
<input type="text"
id="content-title"
name="title"
data-validate='[{"rule":"required"},{"rule":"minLength","params":"10"},{"rule":"maxLength","params":"100"}]'
class="form-control"
placeholder="Enter a descriptive title">
</div>
<div class="form-group">
<label for="content-category">Category *</label>
<select id="content-category"
name="category"
data-validate='[{"rule":"required"}]'
class="form-control">
<option value="">Select a category...</option>
<option value="tutorial">Tutorial</option>
<option value="guide">Guide</option>
<option value="reference">Reference</option>
<option value="example">Example</option>
</select>
</div>
<div class="form-group">
<label for="content-tags">Tags (comma-separated)</label>
<input type="text"
id="content-tags"
name="tags"
data-validate='[{"rule":"pattern","params":"^[a-zA-Z0-9,-\\s]+$"}]'
class="form-control"
placeholder="markdown, tutorial, documentation">
</div>
<div class="form-group">
<label for="markdown-content">Markdown Content *</label>
<textarea id="markdown-content"
name="content"
data-validate='[{"rule":"required"},{"rule":"minLength","params":"100"},{"rule":"markdown"},{"rule":"markdownHeadings"}]'
class="form-control markdown-editor"
rows="20"
placeholder="Enter your Markdown content here...
# Example Heading
Your content goes here with proper **formatting** and [links](http://example.com).
## Code Examples
```javascript
console.log('Hello, world!');
Remember to use proper Markdown syntax!”></textarea>
<div class="form-help">
Write your content in Markdown format. The validator will check for syntax issues and heading structure.
</div>
<div class="markdown-preview" id="markdown-preview">
<h3>Preview</h3>
<div class="preview-content" id="preview-content">
<p>Preview will appear here as you type…</p>
</div>
</div>
</div>
<div class="form-group">
<label for="code-language">Primary Code Language</label>
<select id="code-language" name="code_language" class="form-control">
<option value="">Not applicable</option>
<option value="javascript">JavaScript</option>
<option value="python">Python</option>
<option value="java">Java</option>
<option value="go">Go</option>
<option value="rust">Rust</option>
<option value="typescript">TypeScript</option>
</select>
</div>
<div class="form-group">
<label for="code-example">Code Example</label>
<textarea id="code-example"
name="code_example"
data-validate='[{"rule":"codeBlock","params":"javascript"}]'
class="form-control code-editor"
rows="10"
placeholder="// Optional: Provide a standalone code example function exampleFunction() {
return 'Hello, Markdown!'; }"></textarea>
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label">
<input type="checkbox"
id="accept-license"
name="accept_license"
data-validate='[{"rule":"required"}]'
value="1">
<span class="checkmark"></span>
I agree to license this content under CC BY-SA 4.0 *
</label>
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label">
<input type="checkbox"
id="original-content"
name="original_content"
data-validate='[{"rule":"required"}]'
value="1">
<span class="checkmark"></span>
I confirm this is my original work or properly attributed *
</label>
</div>
<div class="form-actions">
<button type="button" id="preview-btn" class="btn btn-secondary">
Preview Content
</button>
<button type="submit" class="btn btn-primary">
Submit Content
</button>
<button type="reset" class="btn btn-outline">
Reset Form
</button>
</div>
<div id="validation-summary" class="validation-summary" role="alert" aria-live="polite">
<!-- Validation summary will appear here -->
</div>
</form>
<!-- Advanced Form: Documentation Configuration -->
<form id="doc-config-form" data-validate class="markdown-form config-form">
<h2>Documentation Configuration</h2>
<div class="form-group">
<label for="site-title">Site Title *</label>
<input type="text"
id="site-title"
name="site_title"
data-validate='[{"rule":"required"},{"rule":"maxLength","params":"50"}]'
class="form-control">
</div>
<div class="form-group">
<label for="base-url">Base URL *</label>
<input type="url"
id="base-url"
name="base_url"
data-validate='[{"rule":"required"},{"rule":"url"}]'
class="form-control"
placeholder="https://docs.example.com">
</div>
<div class="form-group">
<label for="theme-config">Theme Configuration (JSON)</label>
<textarea id="theme-config"
name="theme_config"
data-validate='[{"rule":"codeBlock","params":"json"}]'
class="form-control code-editor"
rows="15"
placeholder='{ "colors": {
"primary": "#007cba",
"secondary": "#6c757d" }, "fonts": {
"body": "Inter, sans-serif",
"heading": "Poppins, sans-serif",
"code": "JetBrains Mono, monospace" }, "layout": {
"sidebar_width": 280,
"max_content_width": 1200 } }'></textarea>
<div class="form-help">
Configure theme settings in JSON format. Invalid JSON will be highlighted.
</div>
</div>
<div class="form-group">
<label for="build-command">Build Command</label>
<input type="text"
id="build-command"
name="build_command"
class="form-control"
placeholder="npm run build"
value="npm run build">
</div>
<div class="form-group">
<fieldset>
<legend>Features</legend>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" name="features" value="search">
<span class="checkmark"></span>
Enable Search
</label>
</div>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" name="features" value="analytics">
<span class="checkmark"></span>
Analytics Integration
</label>
</div>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" name="features" value="comments">
<span class="checkmark"></span>
Comment System
</label>
</div>
</fieldset>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
Save Configuration
</button>
<button type="button" id="validate-config" class="btn btn-secondary">
Validate Only
</button>
</div>
</form>
<script src="form-validator.js"></script>
<script src="markdown-preview.js"></script>
<script>
// Enhanced form interaction
document.addEventListener('DOMContentLoaded', () => {
// Setup markdown preview
const markdownContent = document.getElementById('markdown-content');
const previewContent = document.getElementById('preview-content');
if (markdownContent && previewContent) {
markdownContent.addEventListener('input', debounce(() => {
updateMarkdownPreview(markdownContent.value, previewContent);
}, 500));
}
// Preview button handler
document.getElementById('preview-btn')?.addEventListener('click', () => {
const content = markdownContent.value;
if (content.trim()) {
openMarkdownPreview(content);
}
});
// Config validation button
document.getElementById('validate-config')?.addEventListener('click', () => {
window.markdownFormValidator.validateForm('doc-config-form');
});
});
function updateMarkdownPreview(markdown, container) {
// Simple markdown to HTML conversion (in real implementation, use a proper parser)
let html = markdown
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank">$1</a>')
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
.replace(/`(.+?)`/g, '<code>$1</code>')
.replace(/\n/g, '<br>');
container.innerHTML = html || '<p><em>Preview will appear here as you type...</em></p>';
}
function openMarkdownPreview(content) {
const previewWindow = window.open('', '_blank', 'width=800,height=600');
previewWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Markdown Preview</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 2rem; }
code { background: #f6f8fa; padding: 0.2em 0.4em; border-radius: 3px; }
pre { background: #f6f8fa; padding: 1rem; border-radius: 6px; overflow: auto; }
blockquote { border-left: 4px solid #dfe2e5; margin: 0; padding-left: 1rem; color: #666; }
</style>
</head>
<body>
${updateMarkdownPreview(content, { innerHTML: '' }).innerHTML || content}
</body>
</html>
`);
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
</script>
</body>
</html>
## Advanced Input Processing Techniques
### Real-Time Content Analysis
Implementing sophisticated content analysis and validation for Markdown input:
```javascript
// content-analyzer.js - Advanced content analysis for Markdown forms
class MarkdownContentAnalyzer {
constructor(options = {}) {
this.options = {
enableReadabilityAnalysis: true,
enableSEOAnalysis: true,
enableAccessibilityCheck: true,
enableContentQualityMetrics: true,
realTimeAnalysis: true,
analysisDebounceDelay: 1000,
...options
};
this.analysisCache = new Map();
this.analysisHistory = [];
this.qualityThresholds = {
readability: 60,
seo: 70,
accessibility: 80,
overall: 70
};
}
async analyzeContent(content, context = {}) {
if (!content || typeof content !== 'string') {
return this.createEmptyAnalysis();
}
const cacheKey = this.generateCacheKey(content, context);
if (this.analysisCache.has(cacheKey)) {
return this.analysisCache.get(cacheKey);
}
const analysis = {
timestamp: new Date().toISOString(),
content: {
length: content.length,
wordCount: this.countWords(content),
paragraphCount: this.countParagraphs(content),
sentenceCount: this.countSentences(content)
},
structure: this.analyzeStructure(content),
readability: this.options.enableReadabilityAnalysis ?
this.analyzeReadability(content) : null,
seo: this.options.enableSEOAnalysis ?
this.analyzeSEO(content, context) : null,
accessibility: this.options.enableAccessibilityCheck ?
this.analyzeAccessibility(content) : null,
quality: this.analyzeContentQuality(content),
issues: [],
suggestions: []
};
// Calculate overall score
analysis.overallScore = this.calculateOverallScore(analysis);
// Generate issues and suggestions
this.generateContentFeedback(analysis);
// Cache the result
this.analysisCache.set(cacheKey, analysis);
// Add to history
this.analysisHistory.push({
timestamp: analysis.timestamp,
contentLength: analysis.content.length,
score: analysis.overallScore
});
// Keep history limited
if (this.analysisHistory.length > 100) {
this.analysisHistory = this.analysisHistory.slice(-50);
}
return analysis;
}
analyzeStructure(content) {
const structure = {
headings: [],
codeBlocks: [],
links: [],
images: [],
lists: [],
tables: []
};
// Extract headings
const headingMatches = content.matchAll(/^(#{1,6})\s+(.+)$/gm);
for (const match of headingMatches) {
structure.headings.push({
level: match[1].length,
text: match[2],
line: this.getLineNumber(content, match.index)
});
}
// Extract code blocks
const codeBlockMatches = content.matchAll(/```(\w+)?\n([\s\S]*?)```/g);
for (const match of codeBlockMatches) {
structure.codeBlocks.push({
language: match[1] || 'plain',
code: match[2],
line: this.getLineNumber(content, match.index)
});
}
// Extract links
const linkMatches = content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g);
for (const match of linkMatches) {
structure.links.push({
text: match[1],
url: match[2],
isExternal: this.isExternalLink(match[2]),
line: this.getLineNumber(content, match.index)
});
}
// Extract images
const imageMatches = content.matchAll(/!\[([^\]]*)\]\(([^)]+)\)/g);
for (const match of imageMatches) {
structure.images.push({
alt: match[1],
src: match[2],
line: this.getLineNumber(content, match.index)
});
}
return structure;
}
analyzeReadability(content) {
// Clean content for readability analysis
const cleanContent = this.cleanContentForAnalysis(content);
const words = this.countWords(cleanContent);
const sentences = this.countSentences(cleanContent);
const syllables = this.countSyllables(cleanContent);
// Calculate Flesch Reading Ease
const fleschScore = this.calculateFleschScore(words, sentences, syllables);
// Calculate Flesch-Kincaid Grade Level
const gradeLevel = this.calculateGradeLevel(words, sentences, syllables);
// Analyze sentence complexity
const sentenceAnalysis = this.analyzeSentenceComplexity(cleanContent);
return {
fleschScore: Math.round(fleschScore),
gradeLevel: Math.round(gradeLevel * 10) / 10,
readingLevel: this.getReadingLevel(fleschScore),
averageWordsPerSentence: Math.round((words / sentences) * 10) / 10,
averageSyllablesPerWord: Math.round((syllables / words) * 100) / 100,
sentenceComplexity: sentenceAnalysis,
recommendations: this.generateReadabilityRecommendations(fleschScore, gradeLevel)
};
}
analyzeSEO(content, context) {
const analysis = {
titleOptimization: this.analyzeTitleSEO(content, context),
headingStructure: this.analyzeHeadingSEO(content),
keywordDensity: this.analyzeKeywordDensity(content, context.keywords),
linkAnalysis: this.analyzeLinkSEO(content),
contentLength: this.analyzeContentLengthSEO(content),
suggestions: []
};
analysis.score = this.calculateSEOScore(analysis);
return analysis;
}
analyzeAccessibility(content) {
const issues = [];
const suggestions = [];
// Check image alt text
const structure = this.analyzeStructure(content);
structure.images.forEach((image, index) => {
if (!image.alt || image.alt.trim() === '') {
issues.push({
type: 'missing_alt_text',
severity: 'high',
line: image.line,
message: 'Image missing alt text for screen readers'
});
} else if (image.alt.length > 125) {
suggestions.push({
type: 'long_alt_text',
line: image.line,
message: 'Alt text is quite long - consider shortening for better accessibility'
});
}
});
// Check heading hierarchy
let lastLevel = 0;
structure.headings.forEach((heading, index) => {
if (heading.level > lastLevel + 1 && lastLevel > 0) {
issues.push({
type: 'heading_hierarchy',
severity: 'medium',
line: heading.line,
message: `Heading level ${heading.level} skips levels (previous was ${lastLevel})`
});
}
lastLevel = heading.level;
});
// Check link text
structure.links.forEach((link, index) => {
const linkText = link.text.toLowerCase();
const genericTerms = ['click here', 'read more', 'more', 'here', 'link'];
if (genericTerms.some(term => linkText.includes(term))) {
suggestions.push({
type: 'generic_link_text',
line: link.line,
message: 'Consider using more descriptive link text'
});
}
});
return {
score: this.calculateAccessibilityScore(issues, suggestions),
issues,
suggestions,
summary: {
totalIssues: issues.length,
highSeverityIssues: issues.filter(i => i.severity === 'high').length,
mediumSeverityIssues: issues.filter(i => i.severity === 'medium').length
}
};
}
analyzeContentQuality(content) {
const quality = {
metrics: {},
scores: {},
recommendations: []
};
// Content depth analysis
const structure = this.analyzeStructure(content);
quality.metrics.structuralDepth = this.calculateStructuralDepth(structure);
// Information density
quality.metrics.informationDensity = this.calculateInformationDensity(content);
// Code-to-text ratio (important for technical documentation)
quality.metrics.codeToTextRatio = this.calculateCodeToTextRatio(content, structure);
// Link diversity
quality.metrics.linkDiversity = this.calculateLinkDiversity(structure.links);
// Content freshness indicators
quality.metrics.freshnessIndicators = this.analyzeFreshnessIndicators(content);
// Calculate individual scores
quality.scores.depth = this.scoreStructuralDepth(quality.metrics.structuralDepth);
quality.scores.density = this.scoreInformationDensity(quality.metrics.informationDensity);
quality.scores.balance = this.scoreCodeToTextRatio(quality.metrics.codeToTextRatio);
quality.scores.engagement = this.scoreLinkDiversity(quality.metrics.linkDiversity);
// Overall quality score
quality.overallScore = Object.values(quality.scores).reduce((sum, score) => sum + score, 0) / Object.keys(quality.scores).length;
return quality;
}
generateContentFeedback(analysis) {
const issues = [];
const suggestions = [];
// Content length feedback
if (analysis.content.wordCount < 300) {
issues.push({
type: 'content_length',
severity: 'medium',
message: 'Content appears quite short. Consider expanding with more details or examples.'
});
} else if (analysis.content.wordCount > 3000) {
suggestions.push({
type: 'content_length',
message: 'Content is quite lengthy. Consider breaking into multiple sections or pages.'
});
}
// Heading structure feedback
if (analysis.structure.headings.length === 0) {
issues.push({
type: 'structure',
severity: 'medium',
message: 'No headings found. Adding headings improves readability and navigation.'
});
}
// Code examples feedback
if (analysis.structure.codeBlocks.length === 0 && analysis.content.wordCount > 500) {
suggestions.push({
type: 'engagement',
message: 'Consider adding code examples to illustrate concepts.'
});
}
// Link analysis feedback
const externalLinks = analysis.structure.links.filter(link => link.isExternal);
if (externalLinks.length > analysis.structure.links.length * 0.8) {
suggestions.push({
type: 'links',
message: 'High ratio of external links. Consider adding internal cross-references.'
});
}
// Readability feedback
if (analysis.readability && analysis.readability.fleschScore < 30) {
issues.push({
type: 'readability',
severity: 'medium',
message: 'Content may be difficult to read. Consider shorter sentences and simpler words.'
});
}
analysis.issues = issues;
analysis.suggestions = suggestions;
}
// Utility methods for analysis calculations
countWords(text) {
return text.trim().split(/\s+/).filter(word => word.length > 0).length;
}
countSentences(text) {
return text.split(/[.!?]+/).filter(sentence => sentence.trim().length > 0).length;
}
countParagraphs(text) {
return text.split(/\n\s*\n/).filter(para => para.trim().length > 0).length;
}
countSyllables(text) {
const words = text.toLowerCase().match(/[a-z]+/g) || [];
return words.reduce((total, word) => {
let syllables = word.match(/[aeiouy]+/g) || [];
if (word.endsWith('e')) syllables = syllables.slice(0, -1);
return total + Math.max(1, syllables.length);
}, 0);
}
calculateFleschScore(words, sentences, syllables) {
return 206.835 - (1.015 * (words / sentences)) - (84.6 * (syllables / words));
}
calculateGradeLevel(words, sentences, syllables) {
return (0.39 * (words / sentences)) + (11.8 * (syllables / words)) - 15.59;
}
getReadingLevel(fleschScore) {
if (fleschScore >= 90) return 'Very Easy';
if (fleschScore >= 80) return 'Easy';
if (fleschScore >= 70) return 'Fairly Easy';
if (fleschScore >= 60) return 'Standard';
if (fleschScore >= 50) return 'Fairly Difficult';
if (fleschScore >= 30) return 'Difficult';
return 'Very Difficult';
}
cleanContentForAnalysis(content) {
return content
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
.replace(/`[^`]+`/g, '') // Remove inline code
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Extract link text
.replace(/!\[[^\]]*\]\([^)]+\)/g, '') // Remove images
.replace(/[#*_\-+>]/g, '') // Remove markdown formatting
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
}
generateCacheKey(content, context) {
const contextString = JSON.stringify(context);
return `${content.length}-${this.simpleHash(content + contextString)}`;
}
simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash);
}
getLineNumber(content, index) {
return content.substring(0, index).split('\n').length;
}
isExternalLink(url) {
return url.startsWith('http') || url.startsWith('//');
}
calculateOverallScore(analysis) {
const scores = [];
if (analysis.readability) {
scores.push(Math.min(100, Math.max(0, analysis.readability.fleschScore)));
}
if (analysis.seo) {
scores.push(analysis.seo.score);
}
if (analysis.accessibility) {
scores.push(analysis.accessibility.score);
}
if (analysis.quality) {
scores.push(analysis.quality.overallScore);
}
return scores.length > 0 ?
scores.reduce((sum, score) => sum + score, 0) / scores.length : 0;
}
createEmptyAnalysis() {
return {
timestamp: new Date().toISOString(),
content: { length: 0, wordCount: 0, paragraphCount: 0, sentenceCount: 0 },
structure: { headings: [], codeBlocks: [], links: [], images: [], lists: [], tables: [] },
readability: null,
seo: null,
accessibility: null,
quality: null,
overallScore: 0,
issues: [],
suggestions: []
};
}
// Public API methods
async analyzeInRealTime(element, context = {}) {
if (!this.options.realTimeAnalysis) return;
const content = element.value || element.textContent;
const analysis = await this.analyzeContent(content, context);
this.displayAnalysisResults(element, analysis);
return analysis;
}
displayAnalysisResults(element, analysis) {
// Create or update analysis display
let analysisDisplay = element.nextElementSibling;
if (!analysisDisplay || !analysisDisplay.classList.contains('content-analysis')) {
analysisDisplay = document.createElement('div');
analysisDisplay.className = 'content-analysis';
element.parentNode.insertBefore(analysisDisplay, element.nextSibling);
}
analysisDisplay.innerHTML = this.generateAnalysisHTML(analysis);
}
generateAnalysisHTML(analysis) {
const scoreClass = this.getScoreClass(analysis.overallScore);
return `
<div class="analysis-summary">
<div class="score-indicator ${scoreClass}">
<span class="score">${Math.round(analysis.overallScore)}</span>
<span class="label">Overall Score</span>
</div>
<div class="metrics">
<div class="metric">
<span class="value">${analysis.content.wordCount}</span>
<span class="label">Words</span>
</div>
<div class="metric">
<span class="value">${analysis.structure.headings.length}</span>
<span class="label">Headings</span>
</div>
<div class="metric">
<span class="value">${analysis.structure.links.length}</span>
<span class="label">Links</span>
</div>
</div>
</div>
${analysis.readability ? `
<div class="readability-info">
<strong>Readability:</strong> ${analysis.readability.readingLevel}
(Grade ${analysis.readability.gradeLevel})
</div>
` : ''}
${analysis.issues.length > 0 ? `
<div class="issues">
<h4>Issues to Address:</h4>
<ul>
${analysis.issues.map(issue => `
<li class="issue ${issue.severity}">${issue.message}</li>
`).join('')}
</ul>
</div>
` : ''}
${analysis.suggestions.length > 0 ? `
<div class="suggestions">
<h4>Suggestions:</h4>
<ul>
${analysis.suggestions.map(suggestion => `
<li class="suggestion">${suggestion.message}</li>
`).join('')}
</ul>
</div>
` : ''}
`;
}
getScoreClass(score) {
if (score >= 80) return 'excellent';
if (score >= 70) return 'good';
if (score >= 60) return 'fair';
return 'needs-improvement';
}
}
// Initialize content analyzer with form validator
document.addEventListener('DOMContentLoaded', () => {
const contentAnalyzer = new MarkdownContentAnalyzer({
realTimeAnalysis: true,
analysisDebounceDelay: 1500
});
// Setup real-time analysis for markdown content areas
const markdownFields = document.querySelectorAll('.markdown-editor');
markdownFields.forEach(field => {
let analysisTimeout;
field.addEventListener('input', () => {
clearTimeout(analysisTimeout);
analysisTimeout = setTimeout(async () => {
const context = {
keywords: field.getAttribute('data-keywords')?.split(',') || [],
category: field.form?.querySelector('[name="category"]')?.value
};
await contentAnalyzer.analyzeInRealTime(field, context);
}, contentAnalyzer.options.analysisDebounceDelay);
});
});
window.markdownContentAnalyzer = contentAnalyzer;
});
Integration with Documentation Systems
Advanced form validation and input handling integrates seamlessly with modern documentation workflows. When combined with automated content validation systems, form validation ensures that user-submitted content meets quality standards and integrates properly with existing documentation structures while maintaining consistency across collaborative editing environments.
For comprehensive content management, form validation complements workflow automation systems by providing structured data collection interfaces that feed into automated processing pipelines, enabling seamless content creation workflows that maintain quality while reducing manual oversight requirements.
When building interactive documentation platforms, form validation works effectively with dynamic content generation systems by providing validated input data that can be safely processed and integrated into generated content, ensuring that user contributions enhance rather than compromise documentation quality and consistency.
Advanced Validation Strategies
Multi-Step Form Validation
Implementing progressive validation for complex documentation workflows:
// multi-step-validator.js - Advanced multi-step form validation system
class MultiStepFormValidator {
constructor(formElement, options = {}) {
this.form = formElement;
this.options = {
saveProgress: true,
validateStepOnNext: true,
allowStepSkipping: false,
progressIndicator: true,
autoSaveInterval: 30000, // 30 seconds
...options
};
this.steps = [];
this.currentStep = 0;
this.stepData = new Map();
this.validationHistory = [];
this.autoSaveTimer = null;
this.init();
}
init() {
this.parseFormSteps();
this.setupProgressIndicator();
this.setupStepNavigation();
this.setupAutoSave();
this.loadSavedProgress();
// Show first step
this.showStep(0);
}
parseFormSteps() {
const stepElements = this.form.querySelectorAll('[data-step]');
stepElements.forEach((element, index) => {
const stepData = {
element: element,
index: index,
title: element.getAttribute('data-step-title') || `Step ${index + 1}`,
description: element.getAttribute('data-step-description') || '',
required: element.hasAttribute('data-step-required'),
fields: Array.from(element.querySelectorAll('[data-validate]')),
validationRules: this.parseStepValidationRules(element),
isValid: false,
isCompleted: false
};
this.steps.push(stepData);
});
console.log(`Initialized ${this.steps.length} form steps`);
}
parseStepValidationRules(stepElement) {
const rules = {
requiredFields: [],
customValidations: [],
dependencies: []
};
// Parse step-level validation rules
const validationData = stepElement.getAttribute('data-step-validation');
if (validationData) {
try {
const parsed = JSON.parse(validationData);
Object.assign(rules, parsed);
} catch (error) {
console.warn('Failed to parse step validation rules:', error);
}
}
// Identify required fields
rules.requiredFields = Array.from(stepElement.querySelectorAll('[required], [data-validate*="required"]'))
.map(field => field.name || field.id);
return rules;
}
setupProgressIndicator() {
if (!this.options.progressIndicator) return;
const progressContainer = document.createElement('div');
progressContainer.className = 'form-progress';
const progressBar = document.createElement('div');
progressBar.className = 'progress-bar';
progressBar.innerHTML = `
<div class="progress-fill" style="width: 0%"></div>
`;
const stepIndicators = document.createElement('div');
stepIndicators.className = 'step-indicators';
this.steps.forEach((step, index) => {
const indicator = document.createElement('div');
indicator.className = 'step-indicator';
indicator.setAttribute('data-step', index);
indicator.innerHTML = `
<div class="step-number">${index + 1}</div>
<div class="step-title">${step.title}</div>
`;
if (index === 0) {
indicator.classList.add('active');
}
stepIndicators.appendChild(indicator);
});
progressContainer.appendChild(progressBar);
progressContainer.appendChild(stepIndicators);
this.form.insertBefore(progressContainer, this.form.firstChild);
this.progressBar = progressBar.querySelector('.progress-fill');
this.stepIndicators = stepIndicators;
}
setupStepNavigation() {
// Create navigation buttons
const navContainer = document.createElement('div');
navContainer.className = 'step-navigation';
navContainer.innerHTML = `
<button type="button" class="btn btn-secondary" id="prev-step">
Previous
</button>
<button type="button" class="btn btn-primary" id="next-step">
Next
</button>
<button type="submit" class="btn btn-success" id="submit-form" style="display: none;">
Submit
</button>
`;
this.form.appendChild(navContainer);
// Setup event listeners
this.prevButton = navContainer.querySelector('#prev-step');
this.nextButton = navContainer.querySelector('#next-step');
this.submitButton = navContainer.querySelector('#submit-form');
this.prevButton.addEventListener('click', () => this.goToPreviousStep());
this.nextButton.addEventListener('click', () => this.goToNextStep());
// Handle form submission
this.form.addEventListener('submit', (event) => {
event.preventDefault();
this.handleFormSubmission();
});
this.updateNavigationButtons();
}
setupAutoSave() {
if (!this.options.saveProgress) return;
this.autoSaveTimer = setInterval(() => {
this.saveProgress();
}, this.options.autoSaveInterval);
// Save on form field changes
this.form.addEventListener('input', this.debounce(() => {
this.saveProgress();
}, 2000));
}
async showStep(stepIndex) {
if (stepIndex < 0 || stepIndex >= this.steps.length) return;
// Hide all steps
this.steps.forEach(step => {
step.element.style.display = 'none';
step.element.classList.remove('active');
});
// Show current step
const currentStep = this.steps[stepIndex];
currentStep.element.style.display = 'block';
currentStep.element.classList.add('active');
this.currentStep = stepIndex;
// Update progress indicator
this.updateProgressIndicator();
this.updateNavigationButtons();
// Focus first field in step
const firstField = currentStep.element.querySelector('input, textarea, select');
if (firstField) {
firstField.focus();
}
// Trigger step change event
this.form.dispatchEvent(new CustomEvent('stepChange', {
detail: {
stepIndex,
step: currentStep,
direction: stepIndex > (this.previousStep || 0) ? 'forward' : 'backward'
}
}));
this.previousStep = stepIndex;
}
async goToNextStep() {
if (this.options.validateStepOnNext) {
const isValid = await this.validateCurrentStep();
if (!isValid) return;
}
if (this.currentStep < this.steps.length - 1) {
await this.showStep(this.currentStep + 1);
}
}
async goToPreviousStep() {
if (this.currentStep > 0) {
await this.showStep(this.currentStep - 1);
}
}
async validateCurrentStep() {
const step = this.steps[this.currentStep];
const validationResults = [];
// Validate all fields in current step
for (const field of step.fields) {
if (window.markdownFormValidator) {
const result = await window.markdownFormValidator.validateField(field, this.form);
validationResults.push(result);
}
}
// Run step-specific validations
const stepValidationResult = await this.runStepValidations(step);
validationResults.push(stepValidationResult);
const isValid = validationResults.every(result => result.valid);
step.isValid = isValid;
if (isValid) {
step.isCompleted = true;
this.markStepAsCompleted(this.currentStep);
} else {
this.markStepAsIncomplete(this.currentStep);
}
return isValid;
}
async runStepValidations(step) {
const validationResult = { valid: true, errors: [] };
// Check required fields
for (const fieldName of step.validationRules.requiredFields) {
const field = step.element.querySelector(`[name="${fieldName}"]`);
if (field) {
const value = this.getFieldValue(field);
if (this.isEmpty(value)) {
validationResult.valid = false;
validationResult.errors.push({
field: fieldName,
message: `${fieldName} is required`
});
}
}
}
// Run custom validations
for (const customValidation of step.validationRules.customValidations) {
try {
const result = await this.runCustomValidation(customValidation, step);
if (!result.valid) {
validationResult.valid = false;
validationResult.errors.push(...result.errors);
}
} catch (error) {
console.error('Custom validation error:', error);
validationResult.valid = false;
validationResult.errors.push({
message: 'Validation failed due to internal error'
});
}
}
return validationResult;
}
async runCustomValidation(validation, step) {
// Example custom validations
switch (validation.type) {
case 'markdown_content_quality':
return await this.validateMarkdownQuality(validation, step);
case 'file_upload_requirements':
return await this.validateFileUploads(validation, step);
case 'content_uniqueness':
return await this.validateContentUniqueness(validation, step);
default:
return { valid: true, errors: [] };
}
}
async validateMarkdownQuality(validation, step) {
const markdownField = step.element.querySelector('.markdown-editor');
if (!markdownField) return { valid: true, errors: [] };
const content = markdownField.value;
const errors = [];
// Check minimum content requirements
const wordCount = content.trim().split(/\s+/).length;
if (wordCount < (validation.minWords || 100)) {
errors.push({
field: markdownField.name,
message: `Content must be at least ${validation.minWords || 100} words`
});
}
// Check for required elements
if (validation.requireHeadings && !content.match(/^#+\s+/m)) {
errors.push({
field: markdownField.name,
message: 'Content must include at least one heading'
});
}
if (validation.requireCodeExamples && !content.match(/```[\s\S]*?```/)) {
errors.push({
field: markdownField.name,
message: 'Content should include code examples'
});
}
return { valid: errors.length === 0, errors };
}
updateProgressIndicator() {
if (!this.progressBar) return;
const progressPercentage = ((this.currentStep + 1) / this.steps.length) * 100;
this.progressBar.style.width = `${progressPercentage}%`;
// Update step indicators
const indicators = this.stepIndicators.querySelectorAll('.step-indicator');
indicators.forEach((indicator, index) => {
indicator.classList.remove('active', 'completed');
if (index === this.currentStep) {
indicator.classList.add('active');
} else if (index < this.currentStep || this.steps[index].isCompleted) {
indicator.classList.add('completed');
}
});
}
updateNavigationButtons() {
this.prevButton.style.display = this.currentStep > 0 ? 'inline-block' : 'none';
this.nextButton.style.display = this.currentStep < this.steps.length - 1 ? 'inline-block' : 'none';
this.submitButton.style.display = this.currentStep === this.steps.length - 1 ? 'inline-block' : 'none';
}
markStepAsCompleted(stepIndex) {
const indicator = this.stepIndicators.querySelector(`[data-step="${stepIndex}"]`);
if (indicator) {
indicator.classList.add('completed');
}
}
markStepAsIncomplete(stepIndex) {
const indicator = this.stepIndicators.querySelector(`[data-step="${stepIndex}"]`);
if (indicator) {
indicator.classList.remove('completed');
}
}
async handleFormSubmission() {
// Validate all steps
for (let i = 0; i < this.steps.length; i++) {
await this.showStep(i);
const isValid = await this.validateCurrentStep();
if (!isValid && this.steps[i].required) {
alert(`Please complete Step ${i + 1}: ${this.steps[i].title}`);
return;
}
}
// Collect all form data
const formData = this.collectAllFormData();
// Submit the form
await this.submitForm(formData);
}
collectAllFormData() {
const formData = new FormData(this.form);
const data = {};
for (const [key, value] of formData.entries()) {
if (data[key]) {
if (Array.isArray(data[key])) {
data[key].push(value);
} else {
data[key] = [data[key], value];
}
} else {
data[key] = value;
}
}
return data;
}
async submitForm(data) {
try {
const response = await fetch(this.form.action || '/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
this.handleSubmissionSuccess();
this.clearSavedProgress();
} else {
this.handleSubmissionError(await response.text());
}
} catch (error) {
this.handleSubmissionError(error.message);
}
}
handleSubmissionSuccess() {
// Show success message
const successDiv = document.createElement('div');
successDiv.className = 'submission-success';
successDiv.innerHTML = `
<h3>✅ Form Submitted Successfully!</h3>
<p>Thank you for your submission. We'll review it shortly.</p>
`;
this.form.innerHTML = '';
this.form.appendChild(successDiv);
}
handleSubmissionError(errorMessage) {
alert(`Submission failed: ${errorMessage}`);
}
saveProgress() {
if (!this.options.saveProgress) return;
const progressData = {
currentStep: this.currentStep,
formData: this.collectAllFormData(),
completedSteps: this.steps.map(step => step.isCompleted),
timestamp: new Date().toISOString()
};
localStorage.setItem(`form-progress-${this.form.id}`, JSON.stringify(progressData));
}
loadSavedProgress() {
if (!this.options.saveProgress) return;
const savedData = localStorage.getItem(`form-progress-${this.form.id}`);
if (!savedData) return;
try {
const progressData = JSON.parse(savedData);
// Restore form data
for (const [fieldName, value] of Object.entries(progressData.formData)) {
const field = this.form.querySelector(`[name="${fieldName}"]`);
if (field) {
if (field.type === 'checkbox' || field.type === 'radio') {
field.checked = field.value === value;
} else {
field.value = value;
}
}
}
// Restore completed steps
progressData.completedSteps.forEach((isCompleted, index) => {
this.steps[index].isCompleted = isCompleted;
});
// Go to saved step
this.showStep(progressData.currentStep);
console.log('Restored form progress from', progressData.timestamp);
} catch (error) {
console.warn('Failed to load saved progress:', error);
}
}
clearSavedProgress() {
localStorage.removeItem(`form-progress-${this.form.id}`);
}
// Utility methods
getFieldValue(field) {
switch (field.type) {
case 'checkbox':
return field.checked;
case 'radio':
const radioGroup = this.form.querySelectorAll(`input[name="${field.name}"]`);
for (const radio of radioGroup) {
if (radio.checked) return radio.value;
}
return null;
case 'file':
return field.files;
default:
return field.value;
}
}
isEmpty(value) {
return value === null || value === undefined ||
(typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0);
}
debounce(func, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Public API
goToStep(stepIndex) {
this.showStep(stepIndex);
}
getCurrentStepData() {
return this.steps[this.currentStep];
}
getFormProgress() {
return {
currentStep: this.currentStep,
totalSteps: this.steps.length,
completedSteps: this.steps.filter(step => step.isCompleted).length,
progressPercentage: ((this.currentStep + 1) / this.steps.length) * 100
};
}
}
// Initialize multi-step forms
document.addEventListener('DOMContentLoaded', () => {
const multiStepForms = document.querySelectorAll('.multi-step-form');
multiStepForms.forEach(form => {
new MultiStepFormValidator(form, {
saveProgress: true,
validateStepOnNext: true,
progressIndicator: true
});
});
});
Accessibility and User Experience
Comprehensive Accessibility Implementation
Ensuring form validation systems work for all users:
/* form-validation-styles.css - Accessible form validation styles */
/* Base form styles */
.markdown-form {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.6;
}
.form-group {
margin-bottom: 1.5rem;
position: relative;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
line-height: 1.5;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
background-color: #fff;
}
.form-control:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Validation states */
.form-control.validation-error {
border-color: #dc2626;
background-color: #fef2f2;
}
.form-control.validation-success {
border-color: #059669;
background-color: #f0fdf4;
}
/* Error and success messages */
.field-error {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 4px;
border-left: 4px solid #dc2626;
}
.error-message {
display: block;
color: #dc2626;
font-size: 0.875rem;
font-weight: 500;
}
.field-warning {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #fffbeb;
border: 1px solid #fed7aa;
border-radius: 4px;
border-left: 4px solid #f59e0b;
}
.warning-message {
display: block;
color: #d97706;
font-size: 0.875rem;
}
/* Labels and help text */
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #374151;
}
.form-help {
margin-top: 0.25rem;
font-size: 0.875rem;
color: #6b7280;
}
/* Button styles */
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
margin-right: 0.5rem;
border: 2px solid transparent;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-primary {
background-color: #3b82f6;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #2563eb;
transform: translateY(-1px);
}
.btn-secondary {
background-color: #6b7280;
color: white;
}
.btn-success {
background-color: #059669;
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
/* Multi-step form progress */
.form-progress {
margin-bottom: 2rem;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #e5e7eb;
border-radius: 4px;
overflow: hidden;
margin-bottom: 1rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #1d4ed8);
transition: width 0.3s ease;
border-radius: 4px;
}
.step-indicators {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
.step-indicator {
flex: 1;
min-width: 120px;
text-align: center;
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
transition: all 0.2s ease;
}
.step-indicator.active {
border-color: #3b82f6;
background-color: #eff6ff;
}
.step-indicator.completed {
border-color: #059669;
background-color: #ecfdf5;
}
.step-number {
display: inline-block;
width: 32px;
height: 32px;
line-height: 28px;
border-radius: 50%;
background-color: #e5e7eb;
color: #374151;
font-weight: 700;
margin-bottom: 0.5rem;
}
.step-indicator.active .step-number {
background-color: #3b82f6;
color: white;
}
.step-indicator.completed .step-number {
background-color: #059669;
color: white;
}
.step-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
}
/* Content analysis display */
.content-analysis {
margin-top: 1rem;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 6px;
background-color: #f9fafb;
}
.analysis-summary {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.score-indicator {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem;
border-radius: 8px;
min-width: 80px;
}
.score-indicator.excellent {
background-color: #dcfce7;
color: #166534;
}
.score-indicator.good {
background-color: #dbeafe;
color: #1e40af;
}
.score-indicator.fair {
background-color: #fef3c7;
color: #a16207;
}
.score-indicator.needs-improvement {
background-color: #fee2e2;
color: #991b1b;
}
.score {
font-size: 1.5rem;
font-weight: 700;
}
.metrics {
display: flex;
gap: 1rem;
flex: 1;
}
.metric {
text-align: center;
}
.metric .value {
display: block;
font-size: 1.25rem;
font-weight: 700;
color: #374151;
}
.metric .label {
font-size: 0.75rem;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Issues and suggestions */
.issues,
.suggestions {
margin-top: 1rem;
}
.issues h4,
.suggestions h4 {
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.issues ul,
.suggestions ul {
margin: 0;
padding-left: 1.5rem;
list-style: none;
}
.issue,
.suggestion {
margin-bottom: 0.5rem;
font-size: 0.875rem;
position: relative;
}
.issue::before {
content: "⚠️";
position: absolute;
left: -1.5rem;
}
.issue.high {
color: #dc2626;
}
.issue.medium {
color: #d97706;
}
.suggestion::before {
content: "💡";
position: absolute;
left: -1.5rem;
}
/* Checkbox and radio styling */
.checkbox-group {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 0.75rem;
cursor: pointer;
font-weight: 400;
line-height: 1.5;
}
.checkbox-label input[type="checkbox"],
.checkbox-label input[type="radio"] {
width: 18px;
height: 18px;
margin: 0;
cursor: pointer;
}
/* Responsive design */
@media (max-width: 768px) {
.markdown-form {
padding: 1rem;
}
.step-indicators {
flex-direction: column;
}
.step-indicator {
min-width: auto;
}
.analysis-summary {
flex-direction: column;
align-items: stretch;
}
.metrics {
justify-content: space-around;
}
.btn {
width: 100%;
margin-bottom: 0.5rem;
margin-right: 0;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.form-control {
border-width: 3px;
}
.form-control:focus {
border-color: #000;
box-shadow: 0 0 0 3px #000;
}
.validation-error {
border-color: #d00;
background-color: #fee;
}
.validation-success {
border-color: #0a0;
background-color: #efe;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.form-control,
.btn,
.step-indicator,
.progress-fill {
transition: none;
}
.btn:hover:not(:disabled) {
transform: none;
}
}
/* Print styles */
@media print {
.btn,
.step-navigation,
.content-analysis {
display: none;
}
.form-control {
border: 1px solid #000;
background: transparent;
}
.validation-error,
.validation-success {
border: 2px solid #000;
}
}
/* Focus indicators for keyboard navigation */
.form-control:focus,
.btn:focus,
.checkbox-label input:focus + .checkmark,
.step-indicator:focus {
outline: 3px solid #3b82f6;
outline-offset: 2px;
}
/* Screen reader only content */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Animation for validation feedback */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.validation-error.shake {
animation: shake 0.5s ease-in-out;
}
/* Loading states */
.btn.loading {
position: relative;
color: transparent;
}
.btn.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
margin-top: -8px;
margin-left: -8px;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
Conclusion
Advanced Markdown form validation and input handling systems provide the foundation for creating interactive, user-friendly documentation platforms that maintain data quality while delivering exceptional user experiences. By implementing comprehensive validation frameworks, real-time content analysis, and accessible form interfaces, technical teams can create documentation systems that not only inform but also effectively collect and validate user input across diverse use cases and user needs.
The key to successful form validation implementation lies in balancing thorough validation with user experience, ensuring that validation systems guide users toward success rather than creating barriers to completion. Whether you’re building simple feedback forms or complex multi-step content submission workflows, the validation techniques and accessibility considerations covered in this guide provide the foundation for creating robust, inclusive, and professional form experiences.
Remember to implement progressive enhancement strategies that work without JavaScript, provide clear and actionable error messages, and continuously test your forms with real users across different devices and assistive technologies. With proper implementation of advanced form validation and input handling, your Markdown documentation can achieve the same level of interactivity and data quality that users expect from modern web applications while maintaining the simplicity and accessibility that makes Markdown such a powerful format for technical communication.