]*>/iu', $matched_text)) {
$offset = $pos + 1;
continue; // Skip this match as it is inside a link or contains a link
}
$matched_fragment = $matched_text;
$after_text = mb_substr($html, $html_end, null, 'UTF-8');
if (mb_strpos($after_text, '.') === 0) {
$trimmed_replacement = rtrim($replacement_html);
if (
preg_match('/\.<\/a>\s*$/ui', $trimmed_replacement) ||
preg_match('/\.\s*$/ui', $trimmed_replacement) ||
preg_match('/<\/a>\.\s*$/ui', $trimmed_replacement)
) {
$after_text = mb_substr($after_text, 1, null, 'UTF-8');
}
}
return mb_substr($html, 0, $html_start, 'UTF-8') . $replacement_html . $after_text;
}
$offset = $pos + 1;
}
return null;
}
/**
* Build a regex pattern for a single normalized word,
* allowing optional intra-word punctuation (apostrophes, hyphens, curly quotes, HTML entities).
*
* @param string $word A normalized word (letters and digits only).
* @return string
*/
private function build_word_pattern($word)
{
$chars = preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY);
if (empty($chars)) {
return '';
}
$intra = "(?:['.,\x{2019}\x{2018}\x{2013}\x{2014}-]|&[^;]+;)*";
$pattern = preg_quote($chars[0], '/');
$count = count($chars);
for ($i = 1; $i < $count; $i++) {
$pattern .= $intra . preg_quote($chars[$i], '/');
}
return $pattern;
}
/**
* Get the inner HTML of a DOMNode.
*
* @param DOMDocument $dom
* @param DOMNode $node
* @return string
*/
private function get_inner_html($dom, $node)
{
$html = '';
foreach ($node->childNodes as $child) {
$html .= $dom->saveHTML($child);
}
return $html;
}
/**
* Replace all content of a DOMNode with new HTML.
*
* @param DOMDocument $dom
* @param DOMNode $node
* @param string $new_html
*/
private function replace_node_content($dom, $node, $new_html)
{
while ($node->firstChild) {
$node->removeChild($node->firstChild);
}
$temp = new DOMDocument();
$temp->loadHTML('' . $new_html . '', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
foreach ($temp->getElementsByTagName('body')->item(0)->childNodes as $child) {
$imported = $dom->importNode($child, true);
$node->appendChild($imported);
}
}
public function verify_paragraph_exists($donor_id, $donor_chunk)
{
$donor_post = get_post($donor_id);
if (!$donor_post || empty($donor_post->post_content)) {
return false;
}
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$html_wrapper = '' . $donor_post->post_content . '
';
$dom->loadHTML($html_wrapper, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$paragraphs = $xpath->query('//p | //li | //blockquote');
$search_text = $this->normalize_text($donor_chunk);
$search_prefix = mb_substr($search_text, 0, 150);
$highest_percent = 0;
foreach ($paragraphs as $p) {
$p_text = $this->normalize_text($p->textContent);
if (mb_strpos($p_text, $search_prefix) !== false) {
return true;
}
similar_text(mb_substr($p_text, 0, 200), mb_substr($search_text, 0, 200), $percent);
if ($percent > $highest_percent) {
$highest_percent = $percent;
}
}
if ($highest_percent > 80) {
return true;
}
// Suffix matching fallback for split/nested list chunks
$search_suffix = mb_substr($search_text, -120);
if (mb_strlen($search_suffix) >= 40) {
foreach ($paragraphs as $p) {
$p_text = $this->normalize_text($p->textContent);
if (mb_strpos($p_text, $search_suffix) !== false) {
return true;
}
}
}
// Prefix matching fallback (shorter block check)
foreach ($paragraphs as $p) {
$p_text = $this->normalize_text($p->textContent);
if (mb_strpos($p_text, mb_substr($search_prefix, 0, 80)) !== false) {
return true;
}
}
return false;
}
private function normalize_text($text)
{
// Add spaces after block tags to avoid word concatenation before stripping tags
$text = preg_replace('/<\/(p|li|ul|ol|blockquote|h[1-6])>/i', '$0 ', $text);
$text = wp_strip_all_tags($text);
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$text = preg_replace('/\s+/u', ' ', $text);
$text = preg_replace('/[^\p{L}\p{N}\s]/u', '', $text);
return trim(mb_strtolower($text, 'UTF-8'));
}
private function urls_are_equal($url1, $url2)
{
$u1 = untrailingslashit(strtolower(trim($url1)));
$u2 = untrailingslashit(strtolower(trim($url2)));
if ($u1 === $u2) {
return true;
}
$path1 = wp_parse_url($u1, PHP_URL_PATH);
$path2 = wp_parse_url($u2, PHP_URL_PATH);
if ($path1 && $path2 && untrailingslashit($path1) === untrailingslashit($path2)) {
return true;
}
return false;
}
}