@wttp/site
Version:
Web3 Transfer Protocol (WTTP) - Site Contracts and deployment tools
1,004 lines (970 loc) โข 46.8 kB
JavaScript
import { task } from "hardhat/config";
import * as fs from "fs";
import * as path from "path";
class NinjaFormsFixer {
constructor(sitePath, createBackups = false) {
this.htmlFiles = [];
this.formsFound = [];
this.sitePath = path.resolve(sitePath);
this.createBackups = createBackups;
}
async fix() {
console.log(`๐ง Fixing Ninja Forms in: ${this.sitePath}`);
// Find all HTML files
this.discoverHtmlFiles();
// Scan for Ninja Forms
await this.scanForNinjaForms();
if (this.formsFound.length === 0) {
console.log("โน๏ธ No Ninja Forms found to fix.");
return;
}
console.log(`๐ Found ${this.formsFound.length} Ninja Form(s) to replace`);
// Replace forms in HTML files
await this.replaceFormsInFiles();
console.log("โ
Ninja Forms replacement complete!");
}
async dryRunAnalysis() {
console.log(`๐ง Analyzing Ninja Forms in: ${this.sitePath}`);
// Find all HTML files
this.discoverHtmlFiles();
// Scan for Ninja Forms
await this.scanForNinjaForms();
if (this.formsFound.length === 0) {
console.log("โน๏ธ No Ninja Forms found to analyze.");
return;
}
console.log(`\n๐ DRY RUN ANALYSIS:`);
console.log(` Forms found: ${this.formsFound.length}`);
this.formsFound.forEach((form, index) => {
console.log(` ${index + 1}. "${form.title}" (ID: ${form.id}) - ${form.fields.length} fields`);
});
console.log(`\n๐ฌ FULL SIMULATION (showing actual impact):`);
// Analyze what would be changed in each file with FULL simulation
for (const htmlFile of this.htmlFiles) {
let content = fs.readFileSync(htmlFile, 'utf-8');
const originalSize = content.length;
let hasNinjaForms = false;
// Test form container replacement
const ninjaFormRegex = /<noscript class="ninja-forms-noscript-message">\s*Notice: JavaScript is required for this content\.\s*<\/noscript>\s*<div\s+id="nf-form-\d+-cont"[^>]*>\s*<div class="nf-loading-spinner"><\/div>\s*<\/div>/g;
const formMatches = content.match(ninjaFormRegex);
if (formMatches) {
hasNinjaForms = true;
console.log(`\n๐ ${path.relative(this.sitePath, htmlFile)}:`);
console.log(` ๐ Original size: ${originalSize.toLocaleString()} characters`);
// Simulate form replacement
let simulatedContent = content;
formMatches.forEach((match, index) => {
// Extract form ID from the match for proper form matching
const formIdMatch = match.match(/nf-form-(\d+)-cont/);
const formIdFromHtml = formIdMatch ? formIdMatch[1] : null;
// Find the corresponding form data
const form = this.formsFound.find(f => f.id.toString() === formIdFromHtml) || this.formsFound[0];
const replacement = this.generateReplacementForm(form);
const position = simulatedContent.indexOf(match);
console.log(` ๐ Form ${index + 1}: Replace ${match.length} chars with ${replacement.length} chars at position ${position.toLocaleString()}`);
console.log(` Original: "${match.substring(0, 50)}${match.length > 50 ? '...' : ''}"`);
console.log(` Replacement: "${replacement.substring(0, 50)}${replacement.length > 50 ? '...' : ''}"`);
simulatedContent = simulatedContent.replace(match, replacement);
});
// Simulate JavaScript removal
console.log(` ๐ฏ JavaScript cleanup simulation:`);
const jsCleanedContent = this.removeNinjaFormsJS(simulatedContent);
const finalSize = jsCleanedContent.length;
const totalChange = finalSize - originalSize;
console.log(` ๐ Final size: ${finalSize.toLocaleString()} characters`);
console.log(` ๐ Net change: ${totalChange > 0 ? '+' : ''}${totalChange.toLocaleString()} characters`);
// Validate critical content preservation
const criticalSections = [
'<!DOCTYPE html>',
'<html',
'<head',
'<body',
'</body>',
'</html>',
'<main',
'wp-site-blocks'
];
const missingCritical = criticalSections.filter(section => content.includes(section) && !jsCleanedContent.includes(section));
if (missingCritical.length > 0) {
console.log(` โ ๏ธ WARNING: Critical content might be removed: ${missingCritical.join(', ')}`);
}
else {
console.log(` โ
Critical page structure preserved`);
}
// Check for potential over-removal
const scriptTagsBefore = (content.match(/<script[^>]*>/g) || []).length;
const scriptTagsAfter = (jsCleanedContent.match(/<script[^>]*>/g) || []).length;
const scriptsRemoved = scriptTagsBefore - scriptTagsAfter;
console.log(` ๐ Script tags: ${scriptTagsBefore} โ ${scriptTagsAfter} (removed: ${scriptsRemoved})`);
if (scriptsRemoved > 5) {
console.log(` โ ๏ธ WARNING: Many script tags removed (${scriptsRemoved}). Review for over-removal.`);
}
if (this.createBackups) {
// Generate proper backup filename: index.html -> index.ninja.html
const dir = path.dirname(htmlFile);
const filename = path.basename(htmlFile, '.html');
const backupPath = path.join(dir, `${filename}.ninja.html`);
console.log(` ๐พ Would create backup: ${path.relative(this.sitePath, backupPath)}`);
const wttpIgnorePath = path.join(this.sitePath, '.wttpignore');
if (fs.existsSync(wttpIgnorePath)) {
const relativePath = path.relative(this.sitePath, backupPath);
console.log(` ๐ Would add to .wttpignore: ${relativePath.replace(/\\/g, '/')}`);
}
}
else {
console.log(` โน๏ธ No backup will be created (use --backup flag to enable)`);
}
}
}
if (!this.htmlFiles.some(file => {
const content = fs.readFileSync(file, 'utf-8');
return /<noscript class="ninja-forms-noscript-message">/.test(content);
})) {
console.log(`\nโจ No files contain Ninja Forms to replace.`);
}
console.log(`\n๐ก To proceed with these changes, run without --dry-run flag.`);
console.log(`\n๐ To apply these changes, run the command again without --dry-run`);
}
discoverHtmlFiles() {
const walkDir = (dir) => {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
walkDir(fullPath);
}
else if (file.name.endsWith('.html') && !file.name.includes('backup') && !file.name.includes('ninja') && !file.name.includes('routes')) {
// Exclude backup files from processing (backup.html, *.backup.html, *.ninja.html, *.routes.html)
this.htmlFiles.push(fullPath);
}
}
};
walkDir(this.sitePath);
console.log(`๐ Found ${this.htmlFiles.length} HTML files to scan`);
}
async scanForNinjaForms() {
for (const htmlFile of this.htmlFiles) {
const content = fs.readFileSync(htmlFile, 'utf-8');
// Look for Ninja Forms JavaScript data - handle both old and new formats
// New format: formContentData in settings object (handles nfForms variable too)
const newFormatRegex = /var\s+formDisplay\s*=\s*1;[\s\S]*?var\s+form\s*=\s*\[\];[\s\S]*?form\.id\s*=\s*['"](\d+)['"];[\s\S]*?form\.settings\s*=\s*(\{[\s\S]*?\});/g;
// Old format: separate form.fields assignment
const oldFormatRegex = /var\s+formDisplay\s*=\s*1;[\s\S]*?var\s+form\s*=\s*\[\];[\s\S]*?form\.id\s*=\s*['"](\d+)['"];[\s\S]*?form\.settings\s*=\s*(\{[\s\S]*?\});[\s\S]*?form\.fields\s*=\s*(\[[\s\S]*?\]);/g;
// Try new format first
let match;
newFormatRegex.lastIndex = 0;
while ((match = newFormatRegex.exec(content)) !== null) {
try {
const formId = match[1];
const settingsStr = match[2];
// Parse settings to get form title and fields
const settings = this.safeJsonParse(settingsStr);
if (settings) {
let parsedFields = [];
// Check if formContentData exists in settings (new format)
if (settings.formContentData && Array.isArray(settings.formContentData)) {
// Convert formContentData array to field objects
parsedFields = this.parseFormContentData(settings.formContentData);
}
if (parsedFields.length > 0) {
this.formsFound.push({
id: formId,
title: settings.title || settings.form_title || 'Contact Form',
fields: parsedFields
});
console.log(`๐ Found form: "${settings.title || settings.form_title}" with ${parsedFields.length} fields in ${path.relative(this.sitePath, htmlFile)}`);
}
}
}
catch (error) {
console.warn(`โ ๏ธ Error parsing form data in ${htmlFile}:`, error);
}
}
// Also look for form.fields array specifically (this contains full field definitions)
const fieldsStartRegex = /form\.fields\s*=\s*\[/g;
let fieldsMatch;
fieldsStartRegex.lastIndex = 0;
while ((fieldsMatch = fieldsStartRegex.exec(content)) !== null) {
console.log(`๐ DEBUG: Found form.fields array at position ${fieldsMatch.index}`);
try {
// Find the matching closing bracket
const startPos = fieldsMatch.index + fieldsMatch[0].length - 1; // Position of opening [
let bracketCount = 1;
let endPos = startPos + 1;
while (endPos < content.length && bracketCount > 0) {
const char = content[endPos];
if (char === '[')
bracketCount++;
else if (char === ']')
bracketCount--;
endPos++;
}
if (bracketCount === 0) {
// Found matching bracket, extract the array
const fieldsStr = content.substring(startPos, endPos);
console.log(`๐ DEBUG: Extracted form.fields array (${fieldsStr.length} chars)`);
console.log(`๐ DEBUG: First 500 chars: ${fieldsStr.substring(0, 500)}`);
console.log(`๐ DEBUG: Last 200 chars: ${fieldsStr.substring(fieldsStr.length - 200)}`);
const fields = this.safeJsonParse(fieldsStr);
console.log(`๐ DEBUG: JSON parse result:`, fields ? `${fields.length} fields` : 'FAILED');
if (!fields) {
console.log(`๐ DEBUG: Raw extracted string for manual inspection:`);
console.log(`๐ DEBUG: "${fieldsStr.substring(0, 1000)}..."`);
}
if (fields && Array.isArray(fields)) {
console.log(`๐ DEBUG: Successfully parsed ${fields.length} fields from form.fields array`);
fields.forEach((field, idx) => {
console.log(`๐ DEBUG: Field ${idx}: type="${field.type}", key="${field.key}", options=${field.options?.length || 0}`);
});
// Find the corresponding form ID by looking for form.id in the same script block
console.log(`๐ DEBUG: Searching for form.id near position ${fieldsMatch.index}`);
const formIdRegex = /form\.id\s*=\s*['"](\d+)['"];/;
const contextStart = Math.max(0, fieldsMatch.index - 50000); // Look back 50000 chars for context
const contextEnd = Math.min(content.length, fieldsMatch.index + fieldsMatch[0].length + 1000); // Look ahead 1000 chars
const contextStr = content.substring(contextStart, contextEnd);
console.log(`๐ DEBUG: Context string (${contextStr.length} chars): "${contextStr.substring(0, 200)}..."`);
const idMatch = formIdRegex.exec(contextStr);
console.log(`๐ DEBUG: Form ID regex match result: ${idMatch ? `found ID "${idMatch[1]}"` : 'NOT FOUND'}`);
if (idMatch) {
const formId = idMatch[1];
// Find title from the same context
const titleRegex = /"(?:title|form_title)":\s*"([^"]+)"/;
const titleMatch = titleRegex.exec(contextStr);
const formTitle = titleMatch ? titleMatch[1] : 'Contact Form';
const parsedFields = this.parseFormFields(fields);
// Check if we already have this form from the new format processing
console.log(`๐ DEBUG: Looking for existing form with ID "${formId}". Current forms: ${this.formsFound.map(f => f.id).join(', ')}`);
const existingForm = this.formsFound.find(f => f.id === formId);
console.log(`๐ DEBUG: Existing form found: ${existingForm ? 'YES' : 'NO'}`);
if (existingForm) {
// Replace with more complete field data
console.log(`๐ DEBUG: BEFORE update - existing form has ${existingForm.fields.length} fields with types: ${existingForm.fields.map(f => f.type).join(', ')}`);
existingForm.fields = parsedFields;
console.log(`๐ DEBUG: AFTER update - existing form has ${existingForm.fields.length} fields with types: ${existingForm.fields.map(f => f.type).join(', ')}`);
console.log(`๐ Updated form: "${formTitle}" with complete field definitions (${parsedFields.length} fields)`);
}
else {
// Add as new form
this.formsFound.push({
id: formId,
title: formTitle,
fields: parsedFields
});
console.log(`๐ Found form: "${formTitle}" with ${parsedFields.length} fields in ${path.relative(this.sitePath, htmlFile)}`);
}
}
}
} // Close bracket for if (bracketCount === 0)
}
catch (error) {
console.warn(`โ ๏ธ Error parsing form.fields data in ${htmlFile}:`, error);
}
}
// Try old format as fallback
oldFormatRegex.lastIndex = 0;
while ((match = oldFormatRegex.exec(content)) !== null) {
try {
const formId = match[1];
const settingsStr = match[2];
const fieldsStr = match[3];
// Parse settings to get form title
const settings = this.safeJsonParse(settingsStr);
const fields = this.safeJsonParse(fieldsStr);
if (settings && fields && Array.isArray(fields)) {
const parsedFields = this.parseFormFields(fields);
this.formsFound.push({
id: formId,
title: settings.title || 'Contact Form',
fields: parsedFields
});
console.log(`๐ Found form: "${settings.title}" with ${parsedFields.length} fields in ${path.relative(this.sitePath, htmlFile)}`);
}
}
catch (error) {
console.warn(`โ ๏ธ Error parsing form data in ${htmlFile}:`, error);
}
}
}
}
safeJsonParse(jsonStr) {
try {
// First try parsing without any cleaning - the form.fields array should be valid JSON
return JSON.parse(jsonStr);
}
catch (e1) {
console.log(`๐ DEBUG: Direct JSON.parse failed: ${e1}`);
try {
// Clean up the JSON string
let cleaned = jsonStr
.replace(/\\\//g, '/') // Fix escaped slashes
.replace(/\\"/g, '"') // Fix escaped quotes
.replace(/(['"])?([a-zA-Z_$][a-zA-Z0-9_$]*)\1?:/g, '"$2":'); // Quote unquoted keys
console.log(`๐ DEBUG: Trying with cleaned JSON...`);
return JSON.parse(cleaned);
}
catch (e2) {
console.log(`๐ DEBUG: Cleaned JSON.parse failed: ${e2}`);
}
// If both parsing attempts fail, try to extract key data manually
if (jsonStr.includes('"formContentData"')) {
const formContentMatch = jsonStr.match(/"formContentData":\s*(\[.*?\])/);
const titleMatch = jsonStr.match(/"title":\s*"([^"]+)"/);
const formTitleMatch = jsonStr.match(/"form_title":\s*"([^"]+)"/);
if (formContentMatch) {
try {
const formContentData = JSON.parse(formContentMatch[1]);
return {
title: titleMatch ? titleMatch[1] : (formTitleMatch ? formTitleMatch[1] : 'Contact Form'),
form_title: formTitleMatch ? formTitleMatch[1] : undefined,
formContentData: formContentData
};
}
catch (parseError) {
// Continue to fallback
}
}
}
// Fallback: extract just title
if (jsonStr.includes('"title"')) {
const titleMatch = jsonStr.match(/"title":\s*"([^"]+)"/);
return { title: titleMatch ? titleMatch[1] : 'Contact Form' };
}
return null;
}
}
parseFormFields(fieldsData) {
const fields = [];
for (const field of fieldsData) {
if (field.type === 'submit')
continue; // Skip submit buttons
let options;
// Extract options for radio/select fields
if (field.type === 'listradio' || field.type === 'select' || field.type === 'listselect') {
if (field.options && Array.isArray(field.options)) {
options = field.options.map((opt) => ({
label: opt.label || opt.text || opt.value || 'Option',
value: opt.value || opt.label || 'value',
selected: Boolean(opt.selected)
}));
}
}
fields.push({
id: field.id || fields.length + 1,
type: this.normalizeFieldType(field.type),
label: field.label || field.key || 'Field',
key: field.key || field.label?.toLowerCase().replace(/\s+/g, '_') || `field_${field.id}`,
required: Boolean(field.required),
placeholder: field.placeholder || '',
order: field.order || fields.length + 1,
options: options
});
}
// Sort by order
fields.sort((a, b) => a.order - b.order);
return fields;
}
parseFormContentData(formContentData) {
console.log(`๐ DEBUG: parseFormContentData - processing ${formContentData.length} field keys`);
const fields = [];
for (let i = 0; i < formContentData.length; i++) {
const fieldKey = formContentData[i];
if (fieldKey === 'submit')
continue; // Skip submit buttons
// Generate field info from the key
let fieldType = 'text';
let fieldLabel = this.generateLabelFromKey(fieldKey);
console.log(`๐ DEBUG: Processing field key "${fieldKey}" -> label: "${fieldLabel}", initial type: "${fieldType}"`);
// Detect field type from key patterns
if (fieldKey.includes('email')) {
fieldType = 'email';
}
else if (fieldKey.includes('phone')) {
fieldType = 'tel';
}
else if (fieldKey.includes('message')) {
fieldType = 'textarea';
}
else if (fieldKey.includes('contact_method') || fieldKey.includes('preferred_contact')) {
fieldType = 'select';
}
console.log(`๐ DEBUG: Final field for "${fieldKey}": type="${fieldType}", label="${fieldLabel}"`);
fields.push({
id: i + 1,
type: fieldType,
label: fieldLabel,
key: fieldKey,
required: ['name', 'email'].some(req => fieldKey.includes(req)), // Assume name and email are required
placeholder: '',
order: i + 1
});
}
console.log(`๐ DEBUG: parseFormContentData complete - returning ${fields.length} fields`);
return fields;
}
generateLabelFromKey(key) {
// Convert field keys to readable labels
if (key === 'name')
return 'Name';
if (key === 'email')
return 'Email';
if (key.includes('phone'))
return 'Phone';
if (key.includes('message'))
return 'Message';
if (key.includes('contact_method') || key.includes('preferred_contact'))
return 'Preferred Contact Method';
// Generic conversion: remove numbers, replace underscores with spaces, capitalize
return key
.replace(/_\d+$/, '') // Remove trailing numbers like _1760598589490
.replace(/_/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
}
normalizeFieldType(type) {
const typeMap = {
'textbox': 'text',
'email': 'email',
'phone': 'tel',
'textarea': 'textarea',
'number': 'number',
'url': 'url',
'date': 'date',
'listradio': 'radio',
'listselect': 'select',
'select': 'select',
'checkbox': 'checkbox'
};
return typeMap[type] || 'text';
}
async replaceFormsInFiles() {
for (const htmlFile of this.htmlFiles) {
let content = fs.readFileSync(htmlFile, 'utf-8');
let modified = false;
console.log(`\n๐ Processing file: ${path.relative(this.sitePath, htmlFile)}`);
console.log(`๐ File size: ${content.length} characters`);
// PRECISE: Match the exact Ninja Forms structure we found in the backup
// This matches: noscript + newlines + form div + whitespace + spinner + whitespace + closing div
const ninjaFormRegex = /<noscript class="ninja-forms-noscript-message">\s*Notice: JavaScript is required for this content\.\s*<\/noscript>\s*<div\s+id="nf-form-\d+-cont"[^>]*>\s*<div class="nf-loading-spinner"><\/div>\s*<\/div>/g;
console.log(`๐ Testing Ninja Forms regex pattern...`);
const matches = content.match(ninjaFormRegex);
if (matches) {
console.log(`โ
Found ${matches.length} Ninja Forms container(s)`);
matches.forEach((match, index) => {
console.log(`\n๐ Match ${index + 1}:`);
console.log(` Length: ${match.length} characters`);
console.log(` First 200 chars: ${match.substring(0, 200)}...`);
console.log(` Last 200 chars: ...${match.substring(match.length - 200)}`);
});
}
else {
console.log(`โ No Ninja Forms containers found with current regex`);
// Let's try to find any noscript tags
const noscriptMatches = content.match(/<noscript[^>]*>[\s\S]*?<\/noscript>/g);
if (noscriptMatches) {
console.log(`๐ Found ${noscriptMatches.length} noscript tag(s):`);
noscriptMatches.forEach((match, index) => {
console.log(` ${index + 1}: ${match.substring(0, 100)}...`);
});
}
// Let's try to find any form-related divs
const formDivMatches = content.match(/<div[^>]*(?:nf-form|ninja-form|form)[^>]*>[\s\S]*?<\/div>/g);
if (formDivMatches) {
console.log(`๐ Found ${formDivMatches.length} form-related div(s):`);
formDivMatches.forEach((match, index) => {
console.log(` ${index + 1}: ${match.substring(0, 100)}...`);
});
}
}
content = content.replace(ninjaFormRegex, (match, offset) => {
modified = true;
console.log(`\n๐ REPLACING MATCH at position ${offset}:`);
console.log(`๐ฅ ORIGINAL (${match.length} chars):`);
if (match.length > 400) {
console.log(` First 200: ${match.substring(0, 200)}...`);
console.log(` Last 200: ...${match.substring(match.length - 200)}`);
}
else {
console.log(` ${match}`);
}
// Extract form ID from the match (e.g., "nf-form-4-cont" -> "4")
const formIdMatch = match.match(/nf-form-(\d+)-cont/);
const formIdFromHtml = formIdMatch ? formIdMatch[1] : null;
// Find the corresponding form data
let form = this.formsFound.find(f => f.id.toString() === formIdFromHtml);
// Fallback to first form if no match found
if (!form) {
console.log(`โ ๏ธ No form found for ID ${formIdFromHtml}, using first available form`);
form = this.formsFound[0];
}
else {
console.log(`โ
Found matching form data for ID ${formIdFromHtml}: "${form.title}"`);
}
const replacement = this.generateReplacementForm(form);
console.log(`๐ค REPLACEMENT (${replacement.length} chars):`);
console.log(` First 200: ${replacement.substring(0, 200)}...`);
return replacement;
});
// Remove Ninja Forms JavaScript more carefully
console.log(`\n๐งน Removing Ninja Forms JavaScript...`);
const originalLength = content.length;
content = this.removeNinjaFormsJS(content);
if (content.length !== originalLength) {
console.log(` Removed ${originalLength - content.length} characters of JS`);
}
if (modified) {
let backupPath = null;
// Create backup only if flag is set AND file contains Ninja Forms
if (this.createBackups && matches && matches.length > 0) {
// Generate proper backup filename: index.html -> index.ninja.html
const dir = path.dirname(htmlFile);
const filename = path.basename(htmlFile, '.html');
backupPath = path.join(dir, `${filename}.ninja.html`);
const originalContent = fs.readFileSync(htmlFile, 'utf-8');
fs.writeFileSync(backupPath, originalContent);
console.log(`๐พ Created backup: ${path.relative(this.sitePath, backupPath)}`);
// Add to .wttpignore if it exists
this.addToWttpIgnore(backupPath);
}
fs.writeFileSync(htmlFile, content);
console.log(`โ๏ธ Updated form in ${path.relative(this.sitePath, htmlFile)}`);
console.log(`๐ Final file size: ${content.length} characters`);
}
else {
console.log(`โญ๏ธ No changes made to ${path.relative(this.sitePath, htmlFile)}`);
}
}
}
generateReplacementForm(form) {
const formHtml = `
<div id="custom-form-${form.id}" class="custom-contact-form">
<h3>${form.title}</h3>
<form id="contact-form-${form.id}" class="contact-form" onsubmit="return submitContactForm(event, '${form.id}')">
${form.fields.map(field => this.generateFieldHtml(field)).join('\n ')}
<div class="form-field">
<button type="submit" class="submit-button">
<span class="submit-text">Submit</span>
<span class="submit-loading" style="display: none;">Sending...</span>
</button>
</div>
<div id="form-message-${form.id}" class="form-message" style="display: none;"></div>
</form>
</div>
<style>
.custom-contact-form {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.custom-contact-form h3 {
margin-bottom: 20px;
color: #333;
}
.form-field {
margin-bottom: 20px;
}
.form-field label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-field input,
.form-field textarea,
.form-field select {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
font-family: inherit;
transition: border-color 0.3s ease;
}
.form-field input:focus,
.form-field textarea:focus,
.form-field select:focus {
outline: none;
border-color: #007cba;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.1);
}
.radio-group fieldset {
border: none;
padding: 0;
margin: 0;
}
.radio-group legend {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
font-size: inherit;
}
.radio-option {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.radio-option input[type="radio"] {
width: auto;
margin-right: 8px;
margin-bottom: 0;
}
.radio-option label {
margin: 0;
font-weight: normal;
cursor: pointer;
}
.checkbox-field .checkbox-option {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.checkbox-field input[type="checkbox"] {
width: auto;
margin-right: 8px;
margin-bottom: 0;
}
.checkbox-field label {
margin: 0;
font-weight: normal;
cursor: pointer;
}
.form-field textarea {
min-height: 120px;
resize: vertical;
}
.submit-button {
background: #007cba;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.submit-button:hover {
background: #005a87;
}
.submit-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.form-message {
padding: 12px;
border-radius: 4px;
margin-top: 15px;
}
.form-message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.form-message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.required {
color: #e74c3c;
}
</style>
<script>
async function submitContactForm(event, formId) {
event.preventDefault();
const form = event.target;
const submitButton = form.querySelector('.submit-button');
const submitText = form.querySelector('.submit-text');
const submitLoading = form.querySelector('.submit-loading');
const messageDiv = document.getElementById('form-message-' + formId);
// Disable submit button and show loading
submitButton.disabled = true;
submitText.style.display = 'none';
submitLoading.style.display = 'inline';
messageDiv.style.display = 'none';
// Collect form data
const formData = new FormData(form);
const formObject = {};
for (let [key, value] of formData.entries()) {
formObject[key] = value;
}
try {
// Get current URL host
const currentHost = window.location.hostname;
// Prepare API URL
const apiUrl = \`https://notify.mancino.ca/api/form-response?url=\${encodeURIComponent(currentHost)}\`;
// Submit to your API
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: currentHost,
response: formObject,
form_id: formId,
timestamp: new Date().toISOString()
})
});
if (response.ok) {
// Success
messageDiv.className = 'form-message success';
messageDiv.textContent = 'Thank you! Your message has been sent successfully.';
messageDiv.style.display = 'block';
form.reset();
} else {
throw new Error('Server responded with error: ' + response.status);
}
} catch (error) {
console.error('Form submission error:', error);
messageDiv.className = 'form-message error';
messageDiv.textContent = 'Sorry, there was an error sending your message. Please try again or contact us directly.';
messageDiv.style.display = 'block';
} finally {
// Re-enable submit button
submitButton.disabled = false;
submitText.style.display = 'inline';
submitLoading.style.display = 'none';
}
return false;
}
</script>`;
return formHtml;
}
generateFieldHtml(field) {
const requiredAttr = field.required ? 'required' : '';
const requiredMark = field.required ? '<span class="required">*</span>' : '';
const placeholder = field.placeholder ? `placeholder="${field.placeholder}"` : '';
if (field.type === 'textarea') {
return `
<div class="form-field">
<label for="${field.key}">${field.label}${requiredMark}</label>
<textarea
id="${field.key}"
name="${field.key}"
${placeholder}
${requiredAttr}
></textarea>
</div>`;
}
else if (field.type === 'radio' && field.options) {
// Generate radio button group
const radioOptions = field.options.map((option, index) => {
const checked = option.selected ? 'checked' : '';
return `
<div class="radio-option">
<input
type="radio"
id="${field.key}_${index}"
name="${field.key}"
value="${option.value}"
${checked}
${requiredAttr}
/>
<label for="${field.key}_${index}">${option.label}</label>
</div>`;
}).join('');
return `
<div class="form-field radio-group">
<fieldset>
<legend>${field.label}${requiredMark}</legend>
${radioOptions}
</fieldset>
</div>`;
}
else if (field.type === 'select' && field.options) {
// Generate select dropdown
const selectOptions = field.options.map(option => {
const selected = option.selected ? 'selected' : '';
return `<option value="${option.value}" ${selected}>${option.label}</option>`;
}).join('');
return `
<div class="form-field">
<label for="${field.key}">${field.label}${requiredMark}</label>
<select
id="${field.key}"
name="${field.key}"
${requiredAttr}
>
${selectOptions}
</select>
</div>`;
}
else if (field.type === 'checkbox') {
// Generate checkbox input
return `
<div class="form-field checkbox-field">
<div class="checkbox-option">
<input
type="checkbox"
id="${field.key}"
name="${field.key}"
value="1"
${requiredAttr}
/>
<label for="${field.key}">${field.label}${requiredMark}</label>
</div>
</div>`;
}
else {
return `
<div class="form-field">
<label for="${field.key}">${field.label}${requiredMark}</label>
<input
type="${field.type}"
id="${field.key}"
name="${field.key}"
${placeholder}
${requiredAttr}
/>
</div>`;
}
}
removeNinjaFormsJS(content) {
let modifiedContent = content;
let totalRemoved = 0;
console.log(` ๐ฏ Using SELECTIVE JavaScript removal (targeting Ninja Forms only)`);
// 1. Remove complete Ninja Forms script blocks by ID (handle both single and double quotes)
const nfScriptMatches = modifiedContent.match(/<script[^>]*id=['"][^'"]*nf-[^'"]*['"][^>]*>[\s\S]*?<\/script>/g);
if (nfScriptMatches) {
console.log(` ๐๏ธ Removing ${nfScriptMatches.length} Ninja Forms script block(s) by ID`);
nfScriptMatches.forEach((match, index) => {
console.log(` ${index + 1}: ${match.length} characters (ID-based removal)`);
totalRemoved += match.length;
});
modifiedContent = modifiedContent.replace(/<script[^>]*id=['"][^'"]*nf-[^'"]*['"][^>]*>[\s\S]*?<\/script>/g, '');
}
// 2. Selectively clean inline scripts that contain Ninja Forms content
modifiedContent = modifiedContent.replace(/<script([^>]*)>([\s\S]*?)<\/script>/g, (scriptMatch, scriptAttrs, scriptContent) => {
// Skip external script references (src="...") - these should never be removed unless explicitly Ninja Forms
if (scriptAttrs.includes('src=')) {
// Only remove if it's explicitly a Ninja Forms external script
if (scriptAttrs.includes('ninja-forms') || scriptAttrs.includes('nf-front-end')) {
console.log(` ๐๏ธ Removing Ninja Forms external script: ${scriptAttrs.substring(0, 50)}...`);
totalRemoved += scriptMatch.length;
return '';
}
// Preserve all other external scripts
return scriptMatch;
}
// Process inline scripts
let cleanedContent = scriptContent;
let removedFromThisScript = 0;
// Only process scripts that contain Ninja Forms data
if (cleanedContent.includes('formDisplay') || cleanedContent.includes('nfForms') || cleanedContent.includes('ninja')) {
const originalLength = cleanedContent.length;
// Remove specific Ninja Forms variable declarations
cleanedContent = cleanedContent.replace(/var\s+formDisplay\s*=\s*[^;]+;/g, '');
cleanedContent = cleanedContent.replace(/var\s+nfForms\s*=\s*[^;]+;/g, '');
cleanedContent = cleanedContent.replace(/var\s+form\s*=\s*\[\];[\s\S]*?nfForms\.push\(form\);/g, '');
cleanedContent = cleanedContent.replace(/form\.id\s*=\s*[^;]+;[\s\S]*?form\.fields\s*=\s*\[[^\]]*\];/g, '');
cleanedContent = cleanedContent.replace(/form\.settings\s*=\s*\{[^}]*\};/g, '');
// Remove Ninja Forms specific objects and arrays
cleanedContent = cleanedContent.replace(/nfForms\s*=\s*nfForms\s*\|\|\s*\[\];/g, '');
cleanedContent = cleanedContent.replace(/form\s*=\s*\[\];/g, '');
removedFromThisScript = originalLength - cleanedContent.length;
if (removedFromThisScript > 0) {
console.log(` โ๏ธ Selectively removed ${removedFromThisScript} chars from inline script`);
totalRemoved += removedFromThisScript;
}
// If we removed everything, delete the entire script block
if (cleanedContent.trim().length <= 5) {
console.log(` ๐๏ธ Removing empty Ninja Forms script block (${scriptMatch.length} chars)`);
totalRemoved += scriptMatch.length;
return '';
}
// Return the cleaned script
return `<script${scriptAttrs}>${cleanedContent}</script>`;
}
// Preserve all other inline scripts unchanged
return scriptMatch;
});
// 3. Remove Ninja Forms template scripts (handle both single and double quotes)
const tmplMatches = modifiedContent.match(/<script[^>]*id=['"]tmpl-nf-[^'"]*['"][\s\S]*?<\/script>/g);
if (tmplMatches) {
console.log(` ๐๏ธ Removing ${tmplMatches.length} Ninja Forms template script(s)`);
tmplMatches.forEach((match, index) => {
console.log(` ${index + 1}: ${match.length} characters`);
totalRemoved += match.length;
});
modifiedContent = modifiedContent.replace(/<script[^>]*id=['"]tmpl-nf-[^'"]*['"][\s\S]*?<\/script>/g, '');
}
// 4. Remove Ninja Forms CSS/JS file references
const linkMatches = modifiedContent.match(/<link[^>]*ninja-forms[^>]*>/g);
if (linkMatches) {
console.log(` ๐๏ธ Removing ${linkMatches.length} Ninja Forms CSS link(s)`);
linkMatches.forEach((match) => totalRemoved += match.length);
modifiedContent = modifiedContent.replace(/<link[^>]*ninja-forms[^>]*>/g, '');
}
const scriptRefMatches = modifiedContent.match(/<script[^>]*ninja-forms[^>]*><\/script>/g);
if (scriptRefMatches) {
console.log(` ๐๏ธ Removing ${scriptRefMatches.length} Ninja Forms script reference(s)`);
scriptRefMatches.forEach((match) => totalRemoved += match.length);
modifiedContent = modifiedContent.replace(/<script[^>]*ninja-forms[^>]*><\/script>/g, '');
}
const frontEndMatches = modifiedContent.match(/<script[^>]*nf-front-end[^>]*><\/script>/g);
if (frontEndMatches) {
console.log(` ๐๏ธ Removing ${frontEndMatches.length} nf-front-end script reference(s)`);
frontEndMatches.forEach((match) => totalRemoved += match.length);
modifiedContent = modifiedContent.replace(/<script[^>]*nf-front-end[^>]*><\/script>/g, '');
}
console.log(` ๐ Total JavaScript removed: ${totalRemoved} characters`);
return modifiedContent;
}
addToWttpIgnore(backupPath) {
const wttpIgnorePath = path.join(this.sitePath, '.wttpignore');
if (!fs.existsSync(wttpIgnorePath)) {
return; // No .wttpignore file exists
}
try {
// Get relative path from site root
const relativePath = path.relative(this.sitePath, backupPath);
const entryToAdd = relativePath.replace(/\\/g, '/'); // Use forward slashes for consistency
// Read existing .wttpignore
const existingContent = fs.readFileSync(wttpIgnorePath, 'utf-8');
// Check if already exists
if (existingContent.includes(entryToAdd)) {
console.log(` ๐ Backup already in .wttpignore: ${entryToAdd}`);
return;
}
// Add the backup file to .wttpignore
const newContent = existingContent.trim() + '\n' + entryToAdd + '\n';
fs.writeFileSync(wttpIgnorePath, newContent);
console.log(` ๐ Added to .wttpignore: ${entryToAdd}`);
}
catch (error) {
console.warn(`โ ๏ธ Failed to update .wttpignore: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
task("wp-ninja-fix", "Replace Ninja Forms with custom HTML forms")
.addParam("path", "Path to WordPress site directory")
.addFlag("dryRun", "Show what would be done without making changes")
.addFlag("backup", "Create backup files and add them to .wttpignore")
.setAction(async (taskArgs, hre) => {
const { path: sitePath, dryRun, backup } = taskArgs;
if (!fs.existsSync(sitePath)) {
throw new Error(`Site path does not exist: ${sitePath}`);
}
const fixer = new NinjaFormsFixer(sitePath, backup);
try {
if (dryRun) {
console.log('๐ Dry run mode - no changes will be made');
if (backup) {
console.log('๐พ Backup mode enabled - would create .backup.html files');
}
await fixer.dryRunAnalysis();
return;
}
if (backup) {
console.log('๐พ Backup mode enabled - creating .backup.html files');
}
else {
console.log('โน๏ธ Backup mode disabled - no backup files will be created');
}
await fixer.fix();
}
catch (error) {
console.error('โ Error:', error);
throw error;
}
});
//# sourceMappingURL=wp-ninja-fix.js.map