Ignore white-space should strip following white-space from Difference / Preview / API endpoints

This commit is contained in:
dgtlmoon
2026-02-20 19:16:17 +01:00
parent 7a51f1e4bf
commit b25e242b8d
5 changed files with 75 additions and 1 deletions
+7
View File
@@ -260,7 +260,10 @@ class WatchSingleHistory(Resource):
response = make_response("No content found", 404)
response.mimetype = "text/plain"
else:
from changedetectionio import html_tools
content = watch.get_history_snapshot(timestamp=timestamp)
if self.datastore.data['settings']['application'].get('ignore_whitespace', False):
content = html_tools.rstrip_snapshot_content(content)
response = make_response(content, 200)
response.mimetype = "text/plain"
@@ -328,8 +331,12 @@ class WatchHistoryDiff(Resource):
no_markup = strtobool(request.args.get('no_markup', 'false'))
# Retrieve snapshot contents
from changedetectionio import html_tools
from_version_file_contents = watch.get_history_snapshot(from_timestamp)
to_version_file_contents = watch.get_history_snapshot(to_timestamp)
if self.datastore.data['settings']['application'].get('ignore_whitespace', False):
from_version_file_contents = html_tools.rstrip_snapshot_content(from_version_file_contents)
to_version_file_contents = html_tools.rstrip_snapshot_content(to_version_file_contents)
# Get diff preferences from query parameters (matching UI preferences in DIFF_PREFERENCES_CONFIG)
# Support both 'type' (UI parameter) and 'word_diff' (API parameter) for backward compatibility
@@ -84,6 +84,9 @@ def construct_blueprint(datastore: ChangeDetectionStore):
versions = list(watch.history.keys())
content = watch.get_history_snapshot(timestamp=timestamp)
if datastore.data['settings']['application'].get('ignore_whitespace', False):
content = html_tools.rstrip_snapshot_content(content)
triggered_line_numbers = html_tools.strip_ignore_text(content=content,
wordlist=watch.get('trigger_text'),
mode='line numbers'
+11
View File
@@ -631,6 +631,17 @@ def workarounds_for_obfuscations(content):
return content
def rstrip_snapshot_content(content: str) -> str:
"""Strip trailing whitespace from each line of a snapshot.
Table-layout pages (common on older sites) cause inscriptis to pad every cell
to the maximum column width found in any row, producing lines of thousands of
trailing spaces from simple spacer/nav rows that sit alongside wide content rows.
This should be applied at display/output time, not at storage time.
"""
return '\n'.join(line.rstrip() for line in content.splitlines())
def get_triggered_text(content, trigger_text):
triggered_text = []
result = strip_ignore_text(content=content,
@@ -152,6 +152,11 @@ def render(watch, datastore, request, url_for, render_template, flash, redirect,
logger.error(f"Unable to read watch history from-version for version {from_version}: {str(e)}")
from_version_file_contents = f"Unable to read to-version {from_version}.\n"
if datastore.data['settings']['application'].get('ignore_whitespace', False):
from changedetectionio import html_tools
to_version_file_contents = html_tools.rstrip_snapshot_content(to_version_file_contents)
from_version_file_contents = html_tools.rstrip_snapshot_content(from_version_file_contents)
screenshot_url = watch.get_screenshot()
system_uses_webdriver = datastore.data['settings']['application']['fetch_backend'] == 'html_webdriver'
@@ -8,7 +8,7 @@ import threading
import unittest
from queue import Queue
from changedetectionio.html_tools import html_to_text
from changedetectionio.html_tools import html_to_text, rstrip_snapshot_content
class TestHtmlToText(unittest.TestCase):
@@ -622,6 +622,54 @@ var also = 1;
assert 'Real content after the data attribute' in text
class TestRstripSnapshotContent(unittest.TestCase):
"""Tests for rstrip_snapshot_content — the display-layer whitespace trimmer."""
def test_strips_trailing_spaces(self):
"""Lines with trailing spaces are rstripped."""
content = "hello \nworld\nfoo "
result = rstrip_snapshot_content(content)
assert result == "hello\nworld\nfoo"
def test_preserves_content_without_trailing_whitespace(self):
"""Lines without trailing whitespace are left unchanged."""
content = "line one\nline two\nline three"
assert rstrip_snapshot_content(content) == content
def test_table_padding_artifact(self):
"""Simulates inscriptis table-layout padding: a symbol followed by thousands of spaces."""
padded_line = "▲" + " " * 27000
content = padded_line + "\n" + " " * 27000 + "\nsome real content"
result = rstrip_snapshot_content(content)
lines = result.splitlines()
assert lines[0] == "▲"
assert lines[1] == ""
assert lines[2] == "some real content"
def test_preserves_leading_whitespace(self):
"""Leading whitespace (indentation) is never touched."""
content = " indented line \n another "
result = rstrip_snapshot_content(content)
assert result == " indented line\n another"
def test_empty_string(self):
assert rstrip_snapshot_content("") == ""
def test_html_to_text_does_not_rstrip(self):
"""html_to_text itself must NOT apply rstrip — that's the output layer's job.
A table with a narrow cell next to a wide cell produces trailing padding."""
html = """<html><body>
<table>
<tr><td>narrow</td><td>{}</td></tr>
<tr><td>x</td><td>y</td></tr>
</table>
</body></html>""".format("w" * 500)
text = html_to_text(html)
# At least one line should have trailing spaces (inscriptis table padding)
has_trailing = any(line != line.rstrip() for line in text.splitlines())
assert has_trailing, "Expected inscriptis to produce trailing-space padding on table rows"
if __name__ == '__main__':
# Can run this file directly for quick testing
unittest.main()