diff --git a/lib/Helper/Filter/JSON/FixInstructionsFilter.php b/lib/Helper/Filter/JSON/FixInstructionsFilter.php index 53e131d42..444f729b7 100644 --- a/lib/Helper/Filter/JSON/FixInstructionsFilter.php +++ b/lib/Helper/Filter/JSON/FixInstructionsFilter.php @@ -98,6 +98,11 @@ public function apply(array &$json): bool { continue; } + if ($this->jsonService->isSchemaObject($value, 'HowToTip', false)) { + $instructions[$key] = [$this->extractHowToStep($value)]; + continue; + } + if ($this->jsonService->isSchemaObject($value, 'HowToSection', false)) { $newInstructions = $this->flattenHowToSection($value); $instructions[$key] = $newInstructions; diff --git a/lib/Helper/HTMLParser/HttpJsonLdParser.php b/lib/Helper/HTMLParser/HttpJsonLdParser.php index 80256954c..2de6142cd 100644 --- a/lib/Helper/HTMLParser/HttpJsonLdParser.php +++ b/lib/Helper/HTMLParser/HttpJsonLdParser.php @@ -26,7 +26,7 @@ public function __construct(IL10N $l10n, JsonService $jsonService) { public function parse(\DOMDocument $document, ?string $url): array { $xpath = new \DOMXPath($document); - $json_ld_elements = $xpath->query("//*[@type='application/ld+json']"); + $json_ld_elements = $xpath->query("//*[@type='application/ld+json'] | //script"); foreach ($json_ld_elements as $json_ld_element) { if (!$json_ld_element || !$json_ld_element->nodeValue) { @@ -58,7 +58,12 @@ private function parseJsonLdElement(\DOMNode $node): array { $json = json_decode($string, true); if ($json === null) { - throw new HtmlParsingException($this->l->t('JSON cannot be decoded.')); + $extractedJson = $this->extractNextJsJson($string); + if ($extractedJson === null) { + throw new HtmlParsingException($this->l->t('JSON cannot be decoded.')); + } + + $json = json_decode($extractedJson, true); } if ($json === false || $json === true || !is_array($json)) { @@ -85,6 +90,34 @@ private function parseJsonLdElement(\DOMNode $node): array { throw new HtmlParsingException($this->l->t('No recipe was found.')); } + /** + * Try to extract escaped JSON from a Next.js flight payload. + * + * Example: + * self.__next_f.push([1,"{\"@context\":\"https://schema.org\",...}"]) + * + * @param string $rawContent + * @return string|null + */ + private function extractNextJsJson(string $rawContent): ?string { + if (strpos($rawContent, 'self.__next_f.push(') === false) { + return null; + } + + $matches = []; + $matched = preg_match('/self\.__next_f\.push\(\[\s*\d+\s*,\s*"((?:\\\\.|[^"\\\\])*)"/s', $rawContent, $matches); + if ($matched !== 1 || !isset($matches[1])) { + return null; + } + + $decoded = stripcslashes($matches[1]); + if ($decoded === '') { + return null; + } + + return $decoded; + } + /** * Fix any JSON issues before trying to decode it * diff --git a/tests/Unit/Helper/HTMLParser/HttpJsonLdParserTest.php b/tests/Unit/Helper/HTMLParser/HttpJsonLdParserTest.php index f34581b69..ac6d74926 100644 --- a/tests/Unit/Helper/HTMLParser/HttpJsonLdParserTest.php +++ b/tests/Unit/Helper/HTMLParser/HttpJsonLdParserTest.php @@ -28,6 +28,7 @@ public static function dataProvider(): array { 'caseJ' => ['caseJ.html', true, 'caseJ.json'], //'caseK' => ['caseK.html', true, 'caseK.json'], 'caseL' => ['caseL.html', true, 'caseL.json'], + 'caseM' => ['caseM.html', true, 'caseM.json'], ]; } @@ -70,6 +71,7 @@ public function testHTMLFile($file, $valid, $jsonFile): void { $parser = new HttpJsonLdParser($l, $jsonService); $content = file_get_contents(__DIR__ . "/res_JsonLd/$file"); + $content = $this->normalizeLineEndings($content); $document = new \DOMDocument(); $document->loadHTML($content); @@ -78,6 +80,7 @@ public function testHTMLFile($file, $valid, $jsonFile): void { $res = $parser->parse($document, 'http://example.com'); $jsonDest = file_get_contents(__DIR__ . "/res_JsonLd/$jsonFile"); + $jsonDest = $this->normalizeLineEndings($jsonDest); $expected = json_decode($jsonDest, true); $this->assertEquals($expected, $res); @@ -86,4 +89,8 @@ public function testHTMLFile($file, $valid, $jsonFile): void { $this->assertFalse($valid); } } + + function normalizeLineEndings(string $text): string { + return str_replace(["\r\n", "\r"], "\n", $text); + } } diff --git a/tests/Unit/Helper/HTMLParser/HttpMicrodataParserTest.php b/tests/Unit/Helper/HTMLParser/HttpMicrodataParserTest.php index 23803ed04..d405e79cc 100644 --- a/tests/Unit/Helper/HTMLParser/HttpMicrodataParserTest.php +++ b/tests/Unit/Helper/HTMLParser/HttpMicrodataParserTest.php @@ -63,15 +63,41 @@ public function testHTMLFile($filename, $valid, $jsonFile, $skipped = false): vo $parser = new HttpMicrodataParser($l); $content = file_get_contents(__DIR__ . "/res_Microdata/$filename"); + $content = $this->normalizeLineEndings($content); $document = new \DOMDocument(); $document->loadHTML($content); try { $res = $parser->parse($document, 'http://example.com'); + if (isset($res['recipeIngredient'])) { + $res['recipeIngredient'] = array_map( + fn($line) => str_replace(["\r\n", "\r"], "\n", $line), + $res['recipeIngredient'] + ); + } + if (isset($res['recipeInstructions'])) { + $res['recipeInstructions'] = array_map( + fn($line) => str_replace(["\r\n", "\r"], "\n", $line), + $res['recipeInstructions'] + ); + } $jsonDest = file_get_contents(__DIR__ . "/res_Microdata/$jsonFile"); + $jsonDest = $this->normalizeLineEndings($jsonDest); $expected = json_decode($jsonDest, true); + if (isset($expected['recipeIngredient'])) { + $expected['recipeIngredient'] = array_map( + fn($line) => str_replace(["\r\n", "\r"], "\n", $line), + $expected['recipeIngredient'] + ); + } + if (isset($expected['recipeInstructions'])) { + $expected['recipeInstructions'] = array_map( + fn($line) => str_replace(["\r\n", "\r"], "\n", $line), + $expected['recipeInstructions'] + ); + } // $this->markTestSkipped(); @@ -168,4 +194,8 @@ private function finishTest($parser, $content, $jsonFile): void { $this->assertFalse(true); } } + + function normalizeLineEndings(string $text): string { + return str_replace(["\r\n", "\r"], "\n", $text); + } } diff --git a/tests/Unit/Helper/HTMLParser/res_JsonLd/caseM.html b/tests/Unit/Helper/HTMLParser/res_JsonLd/caseM.html new file mode 100644 index 000000000..dc7e6680d --- /dev/null +++ b/tests/Unit/Helper/HTMLParser/res_JsonLd/caseM.html @@ -0,0 +1,702 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tex-Mex-Burger mit Guacamole | Kochen Mit Picnic + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+
+ + + + + + + Tex-Mex-Burger mit Guacamole + +
+
+
+
+ +
+ + + 25 min + +
+

+ Tex-Mex-Burger mit Guacamole +

+

Mexiko trifft USA und das Resultat ist: Muy tasty!

+
+
+ +

Zutaten

+
+
+ +

Zubereitung

+
+
+
+
+
+
+
+ Jalapeño-Scheiben +
+
+
Jalapeño-Scheiben
+
50 g
+
+
+
+
+
+
+ Rinderhackfleisch +
+
+
Rinderhackfleisch
+
400 g
+
+
+
+
+
+
+ Sandwich-Käse +
+
+
Sandwich-Käse
+
4 Scheiben
+
+
+
+
+
+
+ Gelbe Paprika +
+
+
Gelbe Paprika
+
1 Stk.
+
+
+
+
+
+
+ Tomaten +
+
+
Tomaten
+
500 g
+
+
+
+
+
+
+ Koriander +
+
+
Koriander
+
1 Bund
+
+
+
+
+
+
+ Limette +
+
+
Limette
+
1 Stk.
+
+
+
+
+
+
+ Avocados +
+
+
Avocados
+
2 Stk.
+
+
+
+
+
+
+ Burgerbrötchen +
+
+
Burgerbrötchen
+
4 Stk.
+
+
+
+
+

Aus der eigenen Küche

+
+
+
+
+ Salz +
+
+
Salz
+
5 g
+
+
+
+
+
+
+ Pfeffer +
+
+
Pfeffer
+
5 g
+
+
+
+
+
+
+ Sonnenblumenöl +
+
+
Sonnenblumenöl
+
2 EL
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+

Wäre das was für dich?

+

Lass dir alles frisch liefern!

+
+ +
+
+
+
+ + + +
+
+
Picnic ausprobieren?
+ App herunterladen + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Unit/Helper/HTMLParser/res_JsonLd/caseM.json b/tests/Unit/Helper/HTMLParser/res_JsonLd/caseM.json new file mode 100644 index 000000000..9f92fefea --- /dev/null +++ b/tests/Unit/Helper/HTMLParser/res_JsonLd/caseM.json @@ -0,0 +1,67 @@ +{ + "@context" : "https://schema.org", + "@type" : "Recipe", + "author" : { + "@type" : "Organization", + "name" : "Picnic", + "url" : "https://picnic.app", + "logo" : "https://picnic.app/_next/static/assets/general/img/picnic-logo.png" + }, + "description" : "Mexiko trifft USA und das Resultat ist: Muy tasty!", + "image" : "https://storefront-prod.de.picnicinternational.com/static/images/recipes/577dd28eac5e3ce9576f4ad64540a0d0d905c6eef8ca1ec4d69abff70a032274/filled-600x600.webp", + "name" : "Tex-Mex-Burger mit Guacamole", + "recipeIngredient" : [ + "50 g Jalapeño-Scheiben", + "400 g Rinderhackfleisch", + "4 Scheiben Sandwich-Käse", + "1 Stk. Gelbe Paprika", + "500 g Tomaten", + "1 Bund Koriander", + "1 Stk. Limette", + "2 Stk. Avocados", + "4 Stk. Burgerbrötchen", + "Nachos", + "Kartoffeln, festkochend", + "5 g Salz", + "5 g Pfeffer", + "2 EL Sonnenblumenöl" + ], + "recipeInstructions" : [ + { + "@type" : "HowToStep", + "name" : "Schritt 1", + "text" : "50 g Jalapenos abtropfen und fein hacken. Jalapenos mit 400 g Rinderhackfleisch, Salz und Pfeffer gleichmäßig vermengen. Die Hackfleischmasse in 4 gleichgroße Patties formen." + }, + { + "@type" : "HowToStep", + "name" : "Schritt 2", + "text" : "In einer Pfanne 2 EL Olivenöl auf mittlerer Stufe erhitzen und Jalapeño-Burger-Patties für 10–12 Min. braten, bis sie braun und durchgegart sind. Regelmäßig wenden. Hitze abschalten, wenn die Burger fertig sind. Auf jeden Burger 1 Scheibe Sandwichkäse legen und kurz bei geschlossener Pfanne schmelzen lassen." + }, + { + "@type" : "HowToStep", + "name" : "Schritt 3", + "text" : "In der Zwischenzeit 1 gelbe Paprika und 500 g Tomaten waschen und in kleine Würfel schneiden. Die Blätter von 1 Bund Koriander ebenfalls waschen und fein hacken. 1/4 der Tomaten und 1/4 des Korianders für die Guacamole beiseitestellen, den Rest mit den Paprikaschoten zu einer Salsa vermischen. 1 Limette auspress, Hälfte des Saftes sowie **Salz und * Pfeffer* zur Salsa geben, gut verrühren." + }, + { + "@type" : "HowToStep", + "name" : "Schritt 4", + "text" : "Das Fruchtfleisch von 2 Avocados mit einer Gabel entfernen und gleichmäßig zerdrücken. Mit den beiseite gelegten Tomaten und dem restlichen Koriander vermischen und mit Salz und Pfeffer und der anderen Hälfte des Limettensafts würzen." + }, + { + "@type" : "HowToStep", + "name" : "Schritt 5", + "text" : "In einer trockenen Pfanne 4 Burgerbrötchen halbieren und auf der Schnittseite toasten, bis die Brötchen leicht gebräunt sind." + }, + { + "@type" : "HowToStep", + "name" : "Schritt 6", + "text" : "Die Brötchen mit einem Burgerpattie, einem Löffel Guacamole und einem Löffel Salsa. Sofort servieren." + }, + { + "@type" : "HowToTip", + "name" : "Tipp", + "text" : "Die Burger zum Beispiel mit Nachos oder würzigen Kartoffeln servieren. Darf es etwas würziger sein? Anschließend noch einige in Scheiben geschnittene Jalapeños zur Salsa hinzufügen." + } + ], + "totalTime" : "PT25M" +}