= $length || ($html[$jsonStart] !== '{' && $html[$jsonStart] !== '[')) { $output .= $attributeToken; $offset = $jsonStart; continue; } $jsonEnd = findJsonEnd($html, $jsonStart); // Could not safely identify a JSON value. Leave this occurrence untouched. if ($jsonEnd === null || $jsonEnd >= $length) { $output .= $attributeToken; $offset = $jsonStart; continue; } // The malformed attribute should end with a literal ". // // Example: // data-mage-init="{"key":"value"}" // ^ if ($html[$jsonEnd] !== '"') { $output .= $attributeToken; $offset = $jsonStart; continue; } // Ensure it's the end of the HTML attribute. $afterJson = $jsonEnd + 1; if ($afterJson < $length) { $nextCharacter = $html[$afterJson]; if ($nextCharacter !== '>' && $nextCharacter !== '/' && !ctype_space($nextCharacter)) { $output .= $attributeToken; $offset = $jsonStart; continue; } } // Extract the JSON from $html $json = substr( $html, $jsonStart, $jsonEnd - $jsonStart ); // Validate that this really is JSON. try { json_decode($json, true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException $e) { $output .= $attributeToken; $offset = $jsonStart; continue; } // JSON normally contains double quotes and no apostrophes, so single quotes are the safest HTML attribute delimiter. if (strpos($json, "'") === false) { $output .= 'data-mage-init=\'' . $json . '\''; } else { // The JSON contains a literal apostrophe, so use double quotes and HTML-escape its double quotes. $escapedJson = htmlspecialchars( $json, ENT_COMPAT | ENT_SUBSTITUTE, 'UTF-8', false ); $output .= 'data-mage-init="' . $escapedJson . '"'; } $changed = true; // Consume the final quote of the malformed attribute. $offset = $afterJson; } return $changed ? $output : $html; } $tests = [ [ 'name' => 'Basic', 'input' => '
', 'expected' => '
', ], [ 'name' => 'Magento JSON', 'input' => '
', 'expected' => '
', ], [ 'name' => 'Nested JSON string', 'input' => '
', 'expected' => '
', ], [ 'name' => 'JSON with apostrophe', 'input' => '
', 'expected' => '
', ], [ 'name' => 'Already valid libxml output', 'input' => '
', 'expected' => '
', ], [ 'name' => 'Other attribute', 'input' => '
', 'expected' => '
', ], [ 'name' => 'Multiple attributes', 'input' => '
' . '
', 'expected' => '
' . '
', ], ]; foreach ($tests as $test) { $actual = repair($test['input']); echo $test['name'] . ': ' . ($actual === $test['expected'] ? 'PASS' : 'FAIL') . PHP_EOL; if ($actual !== $test['expected']) { echo "Expected: " . $test['expected'] . PHP_EOL; echo "Actual: " . $actual . PHP_EOL; } }