From a7e8b072b72af1e23506bbf696ebfd929932d050 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:19:52 -0700 Subject: [PATCH] fix: keep valid JSON-LD when a sibling script is malformed Under errors=ignore/log, one bad application/ld+json script dropped all siblings. Honor the errors policy per script inside JsonLdExtractor. --- extruct/_extruct.py | 2 +- extruct/jsonld.py | 26 +++++++++++++++++++------- tests/test_extruct.py | 21 +++++++++++++++++++++ tests/test_jsonld.py | 26 ++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/extruct/_extruct.py b/extruct/_extruct.py index 51da7f49..d19c804a 100644 --- a/extruct/_extruct.py +++ b/extruct/_extruct.py @@ -99,7 +99,7 @@ def extract( processors.append( ( "json-ld", - JsonLdExtractor().extract_items, + JsonLdExtractor(errors=errors).extract_items, tree, ) ) diff --git a/extruct/jsonld.py b/extruct/jsonld.py index d25a4183..8a1d184e 100644 --- a/extruct/jsonld.py +++ b/extruct/jsonld.py @@ -4,6 +4,7 @@ """ import json +import logging import re import jstyleson @@ -11,6 +12,7 @@ from extruct.utils import parse_html +logger = logging.getLogger(__name__) HTML_OR_JS_COMMENTLINE = re.compile(r"^\s*(//.*|)") @@ -19,18 +21,28 @@ class JsonLdExtractor: 'descendant-or-self::script[@type="application/ld+json"]' ) + def __init__(self, errors="strict"): + self.errors = errors + def extract(self, htmlstring, base_url=None, encoding="UTF-8"): tree = parse_html(htmlstring, encoding=encoding) return self.extract_items(tree, base_url=base_url) def extract_items(self, document, base_url=None): - return [ - item - for items in map(self._extract_items, self._xp_jsonld(document)) # type: ignore[arg-type] - if items - for item in items - if item - ] + items = [] + for node in self._xp_jsonld(document): # type: ignore[arg-type] + try: + for item in self._extract_items(node) or (): + if item: + items.append(item) + except ValueError as e: + if self.errors == "strict": + raise + if self.errors == "log": + logger.exception( + "Failed to extract json-ld script, raises {}".format(e) + ) + return items def _extract_items(self, node): script = node.xpath("string()").strip() diff --git a/tests/test_extruct.py b/tests/test_extruct.py index f7ef6fc3..9c0fe950 100644 --- a/tests/test_extruct.py +++ b/tests/test_extruct.py @@ -92,3 +92,24 @@ def test_errors(self): # ignore exceptions data = extruct.extract(body, errors="log") assert data == {} + + def test_errors_ignore_keeps_valid_jsonld_siblings(self): + body = ( + "" + '" + '' + "" + ) + expected = {"json-ld": [{"@type": "Person", "name": "Ada"}]} + data = extruct.extract(body, errors="ignore", syntaxes=["json-ld"]) + self.assertEqual(data, expected) + + with self.assertLogs("extruct.jsonld", level="ERROR") as cm: + data = extruct.extract(body, errors="log", syntaxes=["json-ld"]) + self.assertEqual(data, expected) + self.assertTrue(any("json-ld script" in line for line in cm.output)) + + with self.assertRaises(ValueError): + extruct.extract(body, errors="strict", syntaxes=["json-ld"]) diff --git a/tests/test_jsonld.py b/tests/test_jsonld.py index 178b3229..f31b998d 100644 --- a/tests/test_jsonld.py +++ b/tests/test_jsonld.py @@ -69,3 +69,29 @@ def test_empty_jsonld_script(self): body = '' data = jsonlde.extract(body) self.assertEqual(data, []) + + def test_malformed_sibling_raises_by_default(self): + jsonlde = JsonLdExtractor() + body = ( + "" + '" + '' + "" + ) + with self.assertRaises(ValueError): + jsonlde.extract(body) + + def test_malformed_sibling_skipped_when_ignoring(self): + body = ( + "" + '" + '' + "" + ) + data = JsonLdExtractor(errors="ignore").extract(body) + self.assertEqual(data, [{"@type": "Person", "name": "Ada"}]) +