216 lines
4.9 KiB
JavaScript
216 lines
4.9 KiB
JavaScript
/**
|
|
* WP Agentic Writer - Utility Functions
|
|
*
|
|
* Pure utility functions with no React dependencies
|
|
* These are shared utilities that can be used by any script
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
*/
|
|
|
|
// Escape HTML to prevent XSS
|
|
const escapeHtml = (value) => {
|
|
if (value === null || value === undefined) return '';
|
|
return String(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
};
|
|
|
|
// Normalize message content (convert objects/arrays to string)
|
|
const normalizeMessageContent = (content) => {
|
|
if (typeof content === 'string' || typeof content === 'number') {
|
|
return String(content);
|
|
}
|
|
return JSON.stringify(content);
|
|
};
|
|
|
|
// Truncate text with ellipsis
|
|
const truncateText = (text, maxLength = 40) => {
|
|
if (!text || text.length <= maxLength) {
|
|
return text;
|
|
}
|
|
return text.substring(0, maxLength) + '...';
|
|
};
|
|
|
|
// Convert markdown to HTML (full renderer)
|
|
const markdownToHtml = (markdown, markdownit, DOMPurify) => {
|
|
const raw = normalizeMessageContent(markdown);
|
|
if (!raw) {
|
|
return '';
|
|
}
|
|
|
|
let rendered = '';
|
|
if (markdownit && DOMPurify) {
|
|
const renderer = markdownit({
|
|
html: false,
|
|
linkify: true,
|
|
typographer: true,
|
|
});
|
|
|
|
if (window.markdownitTaskLists) {
|
|
renderer.use(window.markdownitTaskLists, { enabled: true, label: true, labelAfter: true });
|
|
}
|
|
|
|
rendered = renderer.render(raw);
|
|
|
|
if (DOMPurify.sanitize) {
|
|
rendered = DOMPurify.sanitize(rendered, {
|
|
ADD_TAGS: ['input'],
|
|
ADD_ATTR: ['type', 'checked', 'disabled'],
|
|
});
|
|
}
|
|
}
|
|
|
|
return rendered;
|
|
};
|
|
|
|
// Extract code blocks from HTML
|
|
const extractCodeBlocks = (html) => {
|
|
const codeBlocks = [];
|
|
const preRegex = /<pre[^>]*><code(?:\s+class="language-([^"]*)")?>([\s\S]*?)<\/code><\/pre>/g;
|
|
let match;
|
|
|
|
while ((match = preRegex.exec(html)) !== null) {
|
|
const lang = match[1] || '';
|
|
const code = match[2]
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'");
|
|
codeBlocks.push({ lang, code });
|
|
}
|
|
|
|
return codeBlocks;
|
|
};
|
|
|
|
// Debounce function
|
|
const debounce = (func, wait) => {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
};
|
|
|
|
// Parse outline plan from AI response
|
|
const parseOutlinePlan = (content) => {
|
|
const sections = [];
|
|
const lines = content.split('\n');
|
|
|
|
let currentSection = null;
|
|
let currentSubsection = null;
|
|
|
|
lines.forEach((line) => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) return;
|
|
|
|
// H2 section (## Title)
|
|
const h2Match = trimmed.match(/^##\s+(.+)$/);
|
|
if (h2Match) {
|
|
if (currentSection) {
|
|
sections.push(currentSection);
|
|
}
|
|
currentSection = {
|
|
id: 'section-' + (sections.length + 1),
|
|
title: h2Match[1].trim(),
|
|
subsections: [],
|
|
};
|
|
currentSubsection = null;
|
|
return;
|
|
}
|
|
|
|
// H3 subsection (### Title)
|
|
const h3Match = trimmed.match(/^###\s+(.+)$/);
|
|
if (h3Match && currentSection) {
|
|
currentSubsection = {
|
|
id: 'subsection-' + (currentSection.subsections.length + 1),
|
|
title: h3Match[1].trim(),
|
|
content: '',
|
|
};
|
|
currentSection.subsections.push(currentSubsection);
|
|
return;
|
|
}
|
|
|
|
// Content line
|
|
if (currentSection) {
|
|
if (currentSubsection) {
|
|
currentSubsection.content += (currentSubsection.content ? '\n' : '') + trimmed;
|
|
} else {
|
|
if (!currentSection.content) {
|
|
currentSection.content = trimmed;
|
|
} else {
|
|
currentSection.content += '\n' + trimmed;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (currentSection) {
|
|
sections.push(currentSection);
|
|
}
|
|
|
|
return sections;
|
|
};
|
|
|
|
// Parse FAQ schema from AI response
|
|
const parseFaqSchema = (content) => {
|
|
const faqs = [];
|
|
const faqBlocks = content.split(/\n\s*#{1,2}\s*Q[^\n]*\n/);
|
|
|
|
faqBlocks.slice(1).forEach((block) => {
|
|
const lines = block.trim().split('\n');
|
|
if (lines.length >= 2) {
|
|
const question = lines[0].replace(/^[#*]+\s*/, '').trim();
|
|
const answer = lines.slice(1).join('\n').trim();
|
|
if (question && answer) {
|
|
faqs.push({ question, answer });
|
|
}
|
|
}
|
|
});
|
|
|
|
return faqs;
|
|
};
|
|
|
|
// Extract block preview from content
|
|
const extractBlockPreview = (block) => {
|
|
if (!block) return '';
|
|
|
|
const content = block.innerHTML || block.content || '';
|
|
const text = content.replace(/<[^>]+>/g, '').trim();
|
|
return truncateText(text, 100);
|
|
};
|
|
|
|
// To text value helper
|
|
const toTextValue = (value) => {
|
|
if (typeof value === 'string') return value;
|
|
if (typeof value === 'number') return String(value);
|
|
if (Array.isArray(value)) return value.map(toTextValue).join(', ');
|
|
if (typeof value === 'object' && value !== null) {
|
|
return JSON.stringify(value);
|
|
}
|
|
return '';
|
|
};
|
|
|
|
// Export for use in other modules
|
|
if (typeof window !== 'undefined') {
|
|
window.WPAWUtils = {
|
|
escapeHtml,
|
|
normalizeMessageContent,
|
|
truncateText,
|
|
markdownToHtml,
|
|
extractCodeBlocks,
|
|
debounce,
|
|
parseOutlinePlan,
|
|
parseFaqSchema,
|
|
extractBlockPreview,
|
|
toTextValue,
|
|
};
|
|
}
|