import { ParsedOutput } from './types.js';
/**
* HTML Output Parser for ServiceNow Background Scripts
*
* Parses HTML responses from ServiceNow background script execution to extract
* script output and format it for consumption by AI agents.
*/
/**
* Parse HTML response from ServiceNow background script execution
*
* Extracts script output from HTML response, focusing on:
* - <pre> blocks (typical script output location)
* - Console-like output areas
* - Error messages and stack traces
*
* @param htmlResponse - The HTML response from ServiceNow
* @returns Parsed output with both HTML and cleaned text
*/
export function parseScriptOutput(htmlResponse: string): ParsedOutput {
if (!htmlResponse || typeof htmlResponse !== 'string') {
return {
html: '',
text: '',
};
}
// Extract <pre> blocks (most common location for script output)
const preBlocks = extractPreBlocks(htmlResponse);
// Extract other potential output areas
const otherOutput = extractOtherOutput(htmlResponse);
// Combine and clean the output
const combinedText = combineOutputs(preBlocks, otherOutput);
// Ensure text is always a string, never null or undefined
const safeText = combinedText != null && typeof combinedText === 'string'
? combinedText
: '';
return {
html: htmlResponse,
text: safeText,
};
}
/**
* Extract content from <pre> blocks
*
* @param html - HTML content
* @returns Array of text content from <pre> blocks
*/
function extractPreBlocks(html: string): string[] {
const preRegex = /<pre[^>]*>(.*?)<\/pre>/gis;
const matches: string[] = [];
let match;
while ((match = preRegex.exec(html)) !== null) {
if (match[1]) {
matches.push(cleanHtmlContent(match[1]));
}
}
return matches;
}
/**
* Extract output from other common areas where script output might appear
*
* @param html - HTML content
* @returns Array of text content from other output areas
*/
function extractOtherOutput(html: string): string[] {
const outputs: string[] = [];
// Look for divs with specific classes that might contain output
const outputDivRegex = /<div[^>]*class=['"][^'"]*(?:output|result|console|log)[^'"]*['"][^>]*>(.*?)<\/div>/gis;
let match;
while ((match = outputDivRegex.exec(html)) !== null) {
if (match[1]) {
outputs.push(cleanHtmlContent(match[1]));
}
}
// Look for textarea elements (sometimes used for output)
const textareaRegex = /<textarea[^>]*>(.*?)<\/textarea>/gis;
while ((match = textareaRegex.exec(html)) !== null) {
if (match[1]) {
outputs.push(cleanHtmlContent(match[1]));
}
}
return outputs;
}
/**
* Clean HTML content by removing tags and decoding entities
*
* @param content - HTML content to clean
* @returns Cleaned text content
*/
function cleanHtmlContent(content: string): string {
if (!content) return '';
// Decode HTML entities
let cleaned = content
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/ /g, ' ');
// Remove HTML tags
cleaned = cleaned.replace(/<[^>]*>/g, '');
// Clean up whitespace
cleaned = cleaned
.replace(/\s+/g, ' ')
.replace(/\n\s*\n/g, '\n')
.trim();
return cleaned;
}
/**
* Combine multiple output sources into a single text string
*
* @param preBlocks - Content from <pre> blocks
* @param otherOutput - Content from other output areas
* @returns Combined and formatted output
*/
function combineOutputs(preBlocks: string[], otherOutput: string[]): string {
const allOutputs = [...preBlocks, ...otherOutput];
// Filter out empty outputs and ensure all are strings
const nonEmptyOutputs = allOutputs.filter(output =>
output != null && typeof output === 'string' && output.trim().length > 0
);
if (nonEmptyOutputs.length === 0) {
return '';
}
// If we have multiple outputs, separate them
if (nonEmptyOutputs.length > 1) {
return nonEmptyOutputs.join('\n\n---\n\n');
}
// Ensure we always return a string, never null
const result = nonEmptyOutputs[0];
return result != null && typeof result === 'string' ? result : '';
}
/**
* Extract error information from HTML response
*
* @param html - HTML content
* @returns Error message if found, null otherwise
*/
export function extractErrorMessage(html: string): string | null {
if (!html) return null;
// Look for common error patterns
const errorPatterns = [
/<div[^>]*class=['"][^'"]*error[^'"]*['"][^>]*>(.*?)<\/div>/i,
/<span[^>]*class=['"][^'"]*error[^'"]*['"][^>]*>(.*?)<\/span>/i,
/Error:\s*(.*?)(?:\n|$)/i,
/Exception:\s*(.*?)(?:\n|$)/i,
];
for (const pattern of errorPatterns) {
const match = pattern.exec(html);
if (match && match[1]) {
const errorText = cleanHtmlContent(match[1]);
if (errorText.trim().length > 0) {
return errorText.trim();
}
}
}
return null;
}
/**
* Check if the HTML response indicates a successful execution
*
* @param html - HTML content
* @returns True if execution appears successful
*/
export function isSuccessfulExecution(html: string): boolean {
if (!html) return false;
// Look for success indicators
const successPatterns = [
/success/i,
/completed/i,
/executed/i,
/finished/i,
];
// Look for error indicators
const errorPatterns = [
/error/i,
/exception/i,
/failed/i,
/timeout/i,
];
const hasSuccess = successPatterns.some(pattern => pattern.test(html));
const hasError = errorPatterns.some(pattern => pattern.test(html));
// If we have explicit success indicators and no errors, consider it successful
if (hasSuccess && !hasError) {
return true;
}
// If we have error indicators, consider it failed
if (hasError) {
return false;
}
// Default to successful if we have any content
return html.trim().length > 0;
}
/**
* Get metadata about the parsed output
*
* @param parsedOutput - Parsed output from parseScriptOutput
* @returns Metadata about the output
*/
export function getOutputMetadata(parsedOutput: ParsedOutput): {
hasOutput: boolean;
textLength: number;
htmlLength: number;
hasErrors: boolean;
isSuccessful: boolean;
} {
const hasOutput = parsedOutput.text.trim().length > 0;
const textLength = parsedOutput.text.length;
const htmlLength = parsedOutput.html.length;
const hasErrors = extractErrorMessage(parsedOutput.html) !== null;
const isSuccessful = isSuccessfulExecution(parsedOutput.html);
return {
hasOutput,
textLength,
htmlLength,
hasErrors,
isSuccessful,
};
}