From a555cffebe49f228f6a1970438db8552dfe718f6 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Wed, 1 Oct 2025 19:48:20 +0200 Subject: [PATCH] fix(html): preserve markdown formatting in read-more functionality (#8803) --- prowler/CHANGELOG.md | 1 + prowler/lib/outputs/html/html.py | 66 +++++++++++++++++++++++++++-- tests/lib/outputs/html/html_test.py | 66 +++++++++++++++++++++++++++-- 3 files changed, 125 insertions(+), 8 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 1e6f10e85b..b1446c5a48 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### Fixed - Fix SNS topics showing empty AWS_ResourceID in Quick Inventory output [(#8762)](https://github.com/prowler-cloud/prowler/issues/8762) +- Fix HTML Markdown output for long strings [(#8803)](https://github.com/prowler-cloud/prowler/pull/8803) ## [v5.12.1] (Prowler v5.12.1) diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 4046ebee9f..7955c8a9f4 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -332,10 +332,68 @@ class HTML(Output): var maxLength = 30; // ReadMore ReadLess $(".show-read-more").each(function () { - var myStr = $(this).text(); - if ($.trim(myStr).length > maxLength) { - var newStr = myStr.substring(0, maxLength); - var removedStr = myStr.substring(maxLength, $.trim(myStr).length); + var myStr = $(this).html(); + var textLength = $(this).text().length; + if (textLength > maxLength) { + // Find the position where to cut while preserving HTML tags and breaking at word boundaries + var cutPosition = 0; + var currentLength = 0; + var inTag = false; + var lastWordBoundary = 0; + var tagStack = []; + + for (var i = 0; i < myStr.length; i++) { + if (myStr[i] === '<') { + inTag = true; + // Track opening tags + if (myStr[i + 1] !== '/') { + var tagEnd = myStr.indexOf('>', i); + if (tagEnd !== -1) { + var tagName = myStr.substring(i + 1, tagEnd).split(' ')[0]; + tagStack.push(tagName); + } + } else { + // Closing tag + var tagEnd = myStr.indexOf('>', i); + if (tagEnd !== -1) { + var tagName = myStr.substring(i + 2, tagEnd).split(' ')[0]; + if (tagStack.length > 0) { + tagStack.pop(); + } + } + } + } else if (myStr[i] === '>') { + inTag = false; + } else if (!inTag) { + currentLength++; + // Only consider word boundaries if we're not inside any HTML tags + if (tagStack.length === 0 && (myStr[i] === ' ' || myStr[i] === '.' || myStr[i] === ',' || myStr[i] === ';' || myStr[i] === ':' || myStr[i] === '!' || myStr[i] === '?')) { + lastWordBoundary = i + 1; + } + + if (currentLength >= maxLength) { + // If we're inside HTML tags, find the next closing tag + if (tagStack.length > 0) { + // Find the next closing tag for the current open tag + var nextClosingTag = ''; + var closingTagPos = myStr.indexOf(nextClosingTag, i); + if (closingTagPos !== -1) { + cutPosition = closingTagPos + nextClosingTag.length; + } else { + // If no closing tag found, use current position + cutPosition = i + 1; + } + } else { + // Use the last word boundary if available, otherwise use current position + cutPosition = lastWordBoundary > 0 ? lastWordBoundary : i + 1; + } + break; + } + } + } + + var newStr = myStr.substring(0, cutPosition); + var removedStr = myStr.substring(cutPosition); $(this).empty().html(newStr); $(this).append(' read more...'); $(this).append('' + removedStr + ''); diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py index 12d2374a29..679e8ea475 100644 --- a/tests/lib/outputs/html/html_test.py +++ b/tests/lib/outputs/html/html_test.py @@ -537,10 +537,68 @@ html_footer = """ var maxLength = 30; // ReadMore ReadLess $(".show-read-more").each(function () { - var myStr = $(this).text(); - if ($.trim(myStr).length > maxLength) { - var newStr = myStr.substring(0, maxLength); - var removedStr = myStr.substring(maxLength, $.trim(myStr).length); + var myStr = $(this).html(); + var textLength = $(this).text().length; + if (textLength > maxLength) { + // Find the position where to cut while preserving HTML tags and breaking at word boundaries + var cutPosition = 0; + var currentLength = 0; + var inTag = false; + var lastWordBoundary = 0; + var tagStack = []; + + for (var i = 0; i < myStr.length; i++) { + if (myStr[i] === '<') { + inTag = true; + // Track opening tags + if (myStr[i + 1] !== '/') { + var tagEnd = myStr.indexOf('>', i); + if (tagEnd !== -1) { + var tagName = myStr.substring(i + 1, tagEnd).split(' ')[0]; + tagStack.push(tagName); + } + } else { + // Closing tag + var tagEnd = myStr.indexOf('>', i); + if (tagEnd !== -1) { + var tagName = myStr.substring(i + 2, tagEnd).split(' ')[0]; + if (tagStack.length > 0) { + tagStack.pop(); + } + } + } + } else if (myStr[i] === '>') { + inTag = false; + } else if (!inTag) { + currentLength++; + // Only consider word boundaries if we're not inside any HTML tags + if (tagStack.length === 0 && (myStr[i] === ' ' || myStr[i] === '.' || myStr[i] === ',' || myStr[i] === ';' || myStr[i] === ':' || myStr[i] === '!' || myStr[i] === '?')) { + lastWordBoundary = i + 1; + } + + if (currentLength >= maxLength) { + // If we're inside HTML tags, find the next closing tag + if (tagStack.length > 0) { + // Find the next closing tag for the current open tag + var nextClosingTag = ''; + var closingTagPos = myStr.indexOf(nextClosingTag, i); + if (closingTagPos !== -1) { + cutPosition = closingTagPos + nextClosingTag.length; + } else { + // If no closing tag found, use current position + cutPosition = i + 1; + } + } else { + // Use the last word boundary if available, otherwise use current position + cutPosition = lastWordBoundary > 0 ? lastWordBoundary : i + 1; + } + break; + } + } + } + + var newStr = myStr.substring(0, cutPosition); + var removedStr = myStr.substring(cutPosition); $(this).empty().html(newStr); $(this).append(' read more...'); $(this).append('' + removedStr + '');