All files / Scripts/rules ViolationUtils.ts

94.04% Statements 79/84
81.63% Branches 40/49
100% Functions 14/14
93.75% Lines 75/80

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 1972x                   2x 16x 16x                   2x 8x 3x       5x 5x       18x 7x 7x   7x 7x 7x 5x 5x 5x     11x 11x 13x       5x 5x                   23x 5x                   18x   18x 19x 20x 19x   19x 21x 21x   21x 21x   21x 19x                             18x 4x       14x   14x 14x   14x 5x   5x                     5x 5x     14x     14x 14x 19x 19x           14x           2x       15x   15x 13x   15x 15x   15x         15x 6x 9x   8x 8x 8x 8x 10x 10x 10x 10x 10x               8x 7x             15x    
import { Strings } from "../Strings";
import { RuleViolation, ViolationGroup } from "./types/AnalysisTypes";
import { HeaderSection } from "./types/interfaces";
 
/**
 * Escape HTML and apply content highlighting to show rule violation patterns
 * @param content - The text content to escape and highlight
 * @param violationGroups - Array of violation groups with highlight patterns
 * @returns The HTML-escaped content with highlighting spans applied
 */
export function escapeAndHighlight(content: string, violationGroups: ViolationGroup[]): string {
    const escapedContent = Strings.htmlEncode(content);
    return highlightContent(escapedContent, violationGroups);
}
 
/**
 * Apply highlighting to HTML content without breaking existing HTML tags
 * Use this for content that already contains HTML (like valueUrl with anchor tags)
 * @param htmlContent - HTML content (e.g., content with anchor tags from mapHeaderToURL)
 * @param violationGroups - Array of violation groups with highlight patterns
 * @returns The HTML content with highlighting spans applied to text nodes only
 */
export function highlightHtml(htmlContent: string, violationGroups: ViolationGroup[]): string {
    if (!htmlContent || !violationGroups || violationGroups.length === 0) {
        return htmlContent;
    }
 
    // Create a temporary DOM element to parse the HTML
    const temp = document.createElement("div");
    temp.innerHTML = htmlContent;
 
    // Function to highlight text nodes recursively without breaking HTML structure
    function highlightTextNodes(node: Node): void {
        if (node.nodeType === Node.TEXT_NODE) {
            const text = node.textContent || "";
            Eif (text.trim()) {
                // Escape the text content before highlighting to prevent XSS
                const escapedText = Strings.htmlEncode(text);
                const highlighted = highlightContent(escapedText, violationGroups);
                if (highlighted !== escapedText) {
                    const span = document.createElement("span");
                    span.innerHTML = highlighted;
                    node.parentNode?.replaceChild(span, node);
                }
            }
        } else Eif (node.nodeType === Node.ELEMENT_NODE) {
            const childNodes = Array.from(node.childNodes);
            childNodes.forEach(child => highlightTextNodes(child));
        }
    }
 
    highlightTextNodes(temp);
    return temp.innerHTML;
}
 
/**
 * Apply content highlighting to show rule violation patterns
 * @param content - The text content to highlight (should already be HTML-escaped)
 * @param violationGroups - Array of violation groups with highlight patterns
 * @returns The content with HTML highlighting spans applied
 */
function highlightContent(content: string, violationGroups: ViolationGroup[]): string {
    if (!content || !violationGroups || violationGroups.length === 0) {
        return content;
    }
 
    interface Match {
        start: number;
        end: number;
        text: string;
    }
 
    // Collect all matches first without modifying content
    const allMatches: Match[] = [];
 
    violationGroups.forEach(group => {
        group.violations.forEach(violation => {
            if (violation.highlightPattern) {
                const patterns = violation.highlightPattern.split("|");
 
                patterns.forEach(pattern => {
                    Eif (pattern && pattern.trim()) {
                        const escapedPattern = pattern.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 
                        try {
                            const regex = new RegExp(escapedPattern, "gi");
                            let match;
                            while ((match = regex.exec(content)) !== null) {
                                allMatches.push({
                                    start: match.index,
                                    end: match.index + match[0].length,
                                    text: match[0]
                                });
                            }
                        } catch (error) {
                            console.warn("Invalid regex pattern:", pattern, error);
                        }
                    }
                });
            }
        });
    });
 
    if (allMatches.length === 0) {
        return content;
    }
 
    // Sort by position and merge overlapping matches
    allMatches.sort((a, b) => a.start - b.start);
 
    const mergedMatches: Match[] = [];
    let current = allMatches[0]!;
 
    for (let i = 1; i < allMatches.length; i++) {
        const next = allMatches[i]!;
 
        Iif (next.start < current.end) {
            // Overlapping - extend current if needed
            if (next.end > current.end) {
                current = {
                    start: current.start,
                    end: next.end,
                    text: content.slice(current.start, next.end)
                };
            }
        } else {
            // Non-overlapping - save current and move to next
            mergedMatches.push(current);
            current = next;
        }
    }
    mergedMatches.push(current);
 
    // Build result by inserting spans from end to start (preserves positions)
    let result = content;
    for (let i = mergedMatches.length - 1; i >= 0; i--) {
        const match = mergedMatches[i]!;
        result =
            result.slice(0, match.start) +
            `<span class="highlight-violation">${match.text}</span>` +
            result.slice(match.end);
    }
 
    return result;
}
 
/**
 * Find violations that apply to a specific row by matching section and content
 */
export function getViolationsForRow(
    row: { id?: string; label?: string; valueUrl?: string; value?: string; header?: string; headerName?: string },
    violationGroups: ViolationGroup[]
): RuleViolation[] {
    const matchingViolations: RuleViolation[] = [];
 
    violationGroups.forEach(group => {
        group.violations.forEach(violation => {
            // Check if violation applies to this row via any of its affected sections
            const matchesSection = violation.affectedSections.some(section => {
                const headerSection = section as HeaderSection;
                // Match by section header/name or headerName property
                return headerSection.header === row.label ||
                       headerSection.header === row.header ||
                       headerSection.header === row.headerName;
            });
 
            if (matchesSection) {
                matchingViolations.push(violation);
            } else if (violation.highlightPattern) {
                // Check if violation pattern matches row content
                const content = row.valueUrl || row.value;
                Eif (content) {
                    const patterns = violation.highlightPattern.split("|");
                    const hasMatch = patterns.some(pattern => {
                        Eif (pattern && pattern.trim()) {
                            try {
                                const escapedPattern = pattern.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
                                const regex = new RegExp(escapedPattern, "gi");
                                return regex.test(content);
                            } catch {
                                return false;
                            }
                        }
                        return false;
                    });
 
                    if (hasMatch) {
                        matchingViolations.push(violation);
                    }
                }
            }
        });
    });
 
    return matchingViolations;
}