diff --git a/src/checkmk_weblate_syncer/html_tags.py b/src/checkmk_weblate_syncer/html_tags.py
index 5634b07..54c4bdc 100644
--- a/src/checkmk_weblate_syncer/html_tags.py
+++ b/src/checkmk_weblate_syncer/html_tags.py
@@ -1,4 +1,5 @@
import re
+from dataclasses import dataclass
# keep in sync with tests/pylint/checker_localization.py:HTMLTagsChecker
_TAG_PATTERN = re.compile("<.*?>")
@@ -7,12 +8,18 @@
)
-def forbidden_tags(text: str) -> set[str]:
- return {
- tag
- for tag in re.findall(
- _TAG_PATTERN,
- text,
+@dataclass(frozen=True)
+class ForbiddenTag:
+ line: int
+ tag: str
+
+
+def forbidden_tags(text: str) -> frozenset[ForbiddenTag]:
+ return frozenset(
+ ForbiddenTag(
+ line=text.count("\n", 0, match.start()) + 1,
+ tag=match.group(),
)
- if not re.match(_ALLOWED_TAGS_PATTERN, tag)
- }
+ for match in _TAG_PATTERN.finditer(text)
+ if not _ALLOWED_TAGS_PATTERN.match(match.group())
+ )
diff --git a/src/checkmk_weblate_syncer/portable_object.py b/src/checkmk_weblate_syncer/portable_object.py
index 152bde1..5d95976 100644
--- a/src/checkmk_weblate_syncer/portable_object.py
+++ b/src/checkmk_weblate_syncer/portable_object.py
@@ -1,18 +1,53 @@
import re
+from collections.abc import Iterable
from pathlib import Path
+from .html_tags import ForbiddenTag
-def remove_header(portable_object_content: str) -> str:
- pattern = re.compile(r"^#: .*?:\d+$")
+_SOURCE_REFERENCE_PATTERN = re.compile(r"^#: (.+?:\d+)$")
+
+
+def remove_header(portable_object_content: str) -> tuple[str, int]:
lines = portable_object_content.splitlines()
index_first_source_string_location = 0
for index, line in enumerate(lines):
- if re.match(pattern, line):
+ if _SOURCE_REFERENCE_PATTERN.match(line):
index_first_source_string_location = index
break
- return "\n".join(lines[index_first_source_string_location:]) + (
+ body = "\n".join(lines[index_first_source_string_location:]) + (
"\n" if portable_object_content.endswith("\n") else ""
)
+ return body, index_first_source_string_location
+
+
+def source_references_at(content: str, line: int) -> tuple[str, ...]:
+ lines = content.splitlines()
+ if not 1 <= line <= len(lines):
+ return ()
+ block_start = 0
+ for i in range(line - 1, -1, -1):
+ if lines[i] == "":
+ block_start = i + 1
+ break
+ return tuple(
+ match.group(1)
+ for line_text in lines[block_start:line]
+ if (match := _SOURCE_REFERENCE_PATTERN.match(line_text))
+ )
+
+
+def format_forbidden_tags_error(
+ portable_object_content: str,
+ header_line_count: int,
+ tags: Iterable[ForbiddenTag],
+) -> str:
+ formatted: list[str] = []
+ for tag in sorted(tags, key=lambda t: (t.line, t.tag)):
+ absolute_line = header_line_count + tag.line
+ references = source_references_at(portable_object_content, absolute_line)
+ suffix = f" ({', '.join(references)})" if references else ""
+ formatted.append(f" line {absolute_line}: {tag.tag!r}{suffix}")
+ return "Found forbidden HTML tags:\n" + "\n".join(formatted)
def make_soure_string_locations_relative(
diff --git a/src/checkmk_weblate_syncer/update_sources.py b/src/checkmk_weblate_syncer/update_sources.py
index 31e9ec0..16715df 100644
--- a/src/checkmk_weblate_syncer/update_sources.py
+++ b/src/checkmk_weblate_syncer/update_sources.py
@@ -5,7 +5,11 @@
from .git import commit_and_push_files, repository_in_clean_state
from .html_tags import forbidden_tags
from .logger import LOGGER
-from .portable_object import make_soure_string_locations_relative, remove_header
+from .portable_object import (
+ format_forbidden_tags_error,
+ make_soure_string_locations_relative,
+ remove_header,
+)
def run(config: UpdateSourcesConfig) -> int:
@@ -32,11 +36,15 @@ def run(config: UpdateSourcesConfig) -> int:
raise
LOGGER.info("Checking HTML tags")
- if forbidden_html_tags := forbidden_tags(remove_header(pot_file_content)):
- error_msg = (
- f"Found forbidden HTML tags: {', '.join(sorted(forbidden_html_tags))}"
+ body, header_line_count = remove_header(pot_file_content)
+ if forbidden_html_tags := forbidden_tags(body):
+ raise ValueError(
+ format_forbidden_tags_error(
+ pot_file_content,
+ header_line_count,
+ forbidden_html_tags,
+ ),
)
- raise ValueError(error_msg)
LOGGER.info("Making source string locations relative")
pot_file_content = make_soure_string_locations_relative(
diff --git a/src/checkmk_weblate_syncer/update_translations.py b/src/checkmk_weblate_syncer/update_translations.py
index d93fae3..0592658 100644
--- a/src/checkmk_weblate_syncer/update_translations.py
+++ b/src/checkmk_weblate_syncer/update_translations.py
@@ -11,6 +11,7 @@
from .html_tags import forbidden_tags
from .logger import LOGGER
from .portable_object import (
+ format_forbidden_tags_error,
remove_header,
remove_last_translator,
remove_source_string_locations,
@@ -116,9 +117,14 @@ def _process_po_file_pair(
)
LOGGER.info("Checking HTML tags")
- if forbidden_html_tags := forbidden_tags(remove_header(po_file_content)):
+ body, header_line_count = remove_header(po_file_content)
+ if forbidden_html_tags := forbidden_tags(body):
return _Failure(
- error_message=f"Found forbidden HTML tags: {', '.join(sorted(forbidden_html_tags))}",
+ error_message=format_forbidden_tags_error(
+ po_file_content,
+ header_line_count,
+ forbidden_html_tags,
+ ),
path=locale_po_file,
)
diff --git a/tests/test_html_tags.py b/tests/test_html_tags.py
index 1049c8e..a4517b8 100644
--- a/tests/test_html_tags.py
+++ b/tests/test_html_tags.py
@@ -1,6 +1,6 @@
import pytest
-from checkmk_weblate_syncer.html_tags import forbidden_tags
+from checkmk_weblate_syncer.html_tags import ForbiddenTag, forbidden_tags
@pytest.mark.parametrize(
@@ -25,9 +25,25 @@
pytest.param(
"123 ",
frozenset(
- [""],
+ {
+ ForbiddenTag(line=1, tag=""),
+ },
),
),
+ pytest.param(
+ "ok line\nbad line\nanother bad line",
+ frozenset(
+ {
+ ForbiddenTag(line=2, tag=""),
+ ForbiddenTag(line=3, tag=""),
+ },
+ ),
+ ),
+ pytest.param(
+ '"
Hinweis>: trailing"',
+ frozenset({ForbiddenTag(line=1, tag=">")}),
+ ),
pytest.param(
# pylint: disable=line-too-long
"""#: /home/weblate/checkmk_weblate_sync/git/checkmk/cmk/gui/wato/pages/host_rename.py:640
@@ -54,6 +70,6 @@
)
def test_html_tags_checker(
text: str,
- expected_result: frozenset[str],
+ expected_result: frozenset[ForbiddenTag],
) -> None:
assert forbidden_tags(text) == expected_result
diff --git a/tests/test_portable_object.py b/tests/test_portable_object.py
index fbb5350..d4116df 100644
--- a/tests/test_portable_object.py
+++ b/tests/test_portable_object.py
@@ -1,17 +1,19 @@
from pathlib import Path
+from checkmk_weblate_syncer.html_tags import ForbiddenTag
from checkmk_weblate_syncer.portable_object import (
+ format_forbidden_tags_error,
make_soure_string_locations_relative,
remove_header,
remove_last_translator,
remove_source_string_locations,
+ source_references_at,
)
def test_remove_header() -> None:
- assert (
- remove_header(
- """# Copyright (C) 2019 Checkmk GmbH - License: GNU General Public License v2
+ body, header_line_count = remove_header(
+ """# Copyright (C) 2019 Checkmk GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.
@@ -46,8 +48,10 @@ def test_remove_header() -> None:
msgid " (Duration: %s)"
msgstr ""
""",
- )
- # pylint: disable=line-too-long
+ )
+ # pylint: disable=line-too-long
+ assert (
+ body
== """#: /home/weblate/checkmk_weblate_sync/git/checkmk/cmk/gui/wato/pages/host_rename.py:640
#, python-format
msgid " (%d times)"
@@ -67,6 +71,56 @@ def test_remove_header() -> None:
msgstr ""
"""
)
+ expected_header_line_count = 27
+ assert header_line_count == expected_header_line_count
+
+
+def test_source_references_at() -> None:
+ content = """#: cmk/gui/foo.py:10
+#: cmk/gui/bar.py:20
+msgid "hello"
+msgstr "hallo"
+
+#: cmk/gui/baz.py:30
+msgid "world"
+msgstr "welt"
+"""
+ # msgstr "hallo" is on line 4 → both refs from the first block
+ assert source_references_at(content, 4) == (
+ "cmk/gui/foo.py:10",
+ "cmk/gui/bar.py:20",
+ )
+ # msgstr "welt" is on line 8 → only the ref from the second block
+ assert source_references_at(content, 8) == ("cmk/gui/baz.py:30",)
+ # out-of-range line returns empty
+ assert source_references_at(content, 999) == ()
+
+
+def test_format_forbidden_tags_error() -> None:
+ content = """# header line
+# header line
+
+#: cmk/gui/foo.py:10
+#: cmk/gui/bar.py:20
+msgid "hello"
+msgstr "oops"
+"""
+ # body starts at file line 4 (1-indexed); header_line_count = 3
+ error = format_forbidden_tags_error(
+ content,
+ header_line_count=3,
+ tags=frozenset(
+ {
+ ForbiddenTag(line=4, tag=""),
+ ForbiddenTag(line=4, tag=""),
+ },
+ ),
+ )
+ assert error == (
+ "Found forbidden HTML tags:\n"
+ " line 7: '' (cmk/gui/foo.py:10, cmk/gui/bar.py:20)\n"
+ " line 7: '' (cmk/gui/foo.py:10, cmk/gui/bar.py:20)"
+ )
def test_make_soure_string_locations_relative() -> None: