Markdown Form Validation and Input Handling: Complete Guide for Interactive Documentation with Dynamic Validation Systems
Master advanced Markdown form validation and input handling techniques for creating interactive documentation with comprehensive validation systems, real-time feedback, and user-friendly error handling that ensures data quality and user experience.
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!">
Write your content in Markdown format. The validator will check for syntax issues and heading structure.
Preview
Preview will appear here as you type...
<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!';
}">