HEX
Server: LiteSpeed
System: Linux server490.bertina.biz 5.14.0-687.31.1.el9_8.x86_64 #1 SMP PREEMPT_DYNAMIC Sat Aug 1 05:38:01 EDT 2026 x86_64
User: parskavirpaya (1252)
PHP: 8.1.34
Disabled: mail, show_source, system, shell_exec, passthru, exec, popen
Upload Files
File: /home/parskavirpaya/public_html/wp-content/plugins/wp-parsidate/inc/Helper/NumberConverter.php
<?php
/**
 * Numbers converter for English numbers in HTML content
 * This code generated by ChatGPT! thanks, AI.
 */

declare( strict_types=1 );

namespace WPParsidate\Helper;

use DOMDocument;
use DOMElement;
use DOMNode;
use DOMText;
use DOMXPath;

final class NumberConverter {
  /**
   * Tags to fully protect before DOM parsing.
   * Their content is restored byte-for-byte.
   */
  private const PROTECTED_TAGS = [
    'script',
    'style',
    'pre',
    'code',
    'textarea',
    'noscript',
    'svg',
  ];

  /**
   * Tags whose text descendants should never be converted.
   * Secondary safety layer after protected-block stripping.
   */
  private const SKIP_TEXT_INSIDE_TAGS = [
    'script',
    'style',
    'pre',
    'code',
    'textarea',
    'noscript',
    'kbd',
    'samp',
    'svg',
  ];

  /**
   * Common classes used by code/highlight/editor blocks.
   */
  private const SKIP_CLASSES = [
    'wp-block-code',
    'wp-block-preformatted',
    'code',
    'hljs',
    'prism',
    'highlight',
    'syntaxhighlighter',
    'crayon-syntax',
  ];

  private const ROOT_ID = '__wp_parsidate_persian_number_root__';

  /**
   * Public API.
   *
   * Converts ASCII digits to Persian digits in eligible text nodes only.
   * Returns original content unchanged if conversion is unnecessary or parsing fails.
   */
  public static function convertContent( string $content ): string {
    if ( $content === '' ) {
      return $content;
    }

    // Fast bailouts.
    if ( ! self::containsAsciiDigit( $content ) ) {
      return $content;
    }

    // If your site is Persian-only and you want ALL visible numbers converted,
    // you can remove this check. Keeping it improves performance and avoids
    // converting English-only text blocks.
    if ( ! self::containsPersianOrArabic( $content ) ) {
      return $content;
    }

    $protected = [];
    $working   = self::protectFragileBlocks( $content, $protected );

    // After protection, maybe no visible digits remain.
    if ( ! self::containsAsciiDigit( $working ) ) {
      return self::restoreFragileBlocks( $working, $protected );
    }

    $converted = self::convertHtmlTextNodes( $working );

    if ( $converted === null ) {
      // Fail-safe: return original content, not partially processed content.
      return $content;
    }

    return self::restoreFragileBlocks( $converted, $protected );
  }

  /**
   * Quick ASCII digit test.
   */
  private static function containsAsciiDigit( string $content ): bool {
    return (bool) preg_match( '/[0-9]/', $content );
  }

  /**
   * Check whether the content contains Persian/Arabic script characters.
   *
   * This reduces unnecessary work on English-only content and avoids converting
   * English sections in multilingual pages unless they contain Persian/Arabic text nearby.
   */
  private static function containsPersianOrArabic( string $content ): bool {
    return (bool) preg_match( '/[\x{0600}-\x{06FF}\x{0750}-\x{077F}\x{08A0}-\x{08FF}]/u', $content );
  }

  /**
   * Replace fragile blocks with placeholders so DOM parsing never touches them.
   *
   * @param string $html
   * @param array<string,string> $protected
   *
   * @return string
   */
  private static function protectFragileBlocks( string $html, array &$protected ): string {
    $tags_pattern = implode( '|', array_map( 'preg_quote', self::PROTECTED_TAGS ) );

    $pattern = '~<(' . $tags_pattern . ')\b[^>]*>.*?</\1>~isu';

    return (string) preg_replace_callback(
      $pattern,
      static function ( array $matches ) use ( &$protected ): string {
        $token               = self::makePlaceholder( count( $protected ) );
        $protected[ $token ] = $matches[0];

        return $token;
      },
      $html
    );
  }

  /**
   * Restore protected placeholders.
   *
   * @param array<string,string> $protected
   */
  private static function restoreFragileBlocks( string $html, array $protected ): string {
    if ( empty( $protected ) ) {
      return $html;
    }

    return strtr( $html, $protected );
  }

  private static function makePlaceholder( int $index ): string {
    try {
      $suffix = bin2hex( random_bytes( 8 ) );
    } catch ( \Exception $e ) {
      $suffix = md5( uniqid( (string) $index, true ) );
    }

    return '%%YPPNC_' . $index . '_' . $suffix . '%%';
  }

  /**
   * Parse fragment HTML and convert eligible text nodes.
   *
   * Returns null on failure.
   */
  private static function convertHtmlTextNodes( string $html ): ?string {
    $previous_errors = libxml_use_internal_errors( true );

    $dom = new DOMDocument( '1.0', 'UTF-8' );

    $wrapped = '<?xml encoding="utf-8" ?><div id="' . self::ROOT_ID . '">' . $html . '</div>';

    $flags = 0;
    if ( defined( 'LIBXML_HTML_NOIMPLIED' ) ) {
      $flags |= LIBXML_HTML_NOIMPLIED;
    }
    if ( defined( 'LIBXML_HTML_NODEFDTD' ) ) {
      $flags |= LIBXML_HTML_NODEFDTD;
    }

    $loaded = $dom->loadHTML( $wrapped, $flags );

    if ( ! $loaded ) {
      libxml_clear_errors();
      libxml_use_internal_errors( $previous_errors );

      return null;
    }

    $xpath      = new DOMXPath( $dom );
    $text_nodes = $xpath->query( '//*[@id="' . self::ROOT_ID . '"]//text()' );

    if ( $text_nodes === false ) {
      libxml_clear_errors();
      libxml_use_internal_errors( $previous_errors );

      return null;
    }

    /** @var DOMNode $node */
    foreach ( $text_nodes as $node ) {
      if ( ! $node instanceof DOMText ) {
        continue;
      }

      $value = $node->nodeValue;

      if ( $value === null || $value === '' ) {
        continue;
      }

      // Very cheap skip before expensive checks.
      if ( ! self::containsAsciiDigit( $value ) ) {
        continue;
      }

      // Skip text nodes that do not contain Persian/Arabic text.
      // Remove this if you want all visible numbers converted everywhere.
      if ( ! self::containsPersianOrArabic( $value ) ) {
        continue;
      }

      if ( self::shouldSkipTextNode( $node ) ) {
        continue;
      }

      $node->nodeValue = self::convertNumbersInText( $value );
    }

    $root = $dom->getElementById( self::ROOT_ID );
    if ( ! $root instanceof DOMElement ) {
      libxml_clear_errors();
      libxml_use_internal_errors( $previous_errors );

      return null;
    }

    $result = '';

    foreach ( $root->childNodes as $child ) {
      $result .= $dom->saveHTML( $child );
    }

    libxml_clear_errors();
    libxml_use_internal_errors( $previous_errors );

    // Defensive cleanup in case XML declaration leaks.
    $result = (string) preg_replace( '/^<\?xml.+?\?>/u', '', $result );

    return $result;
  }

  private static function shouldSkipTextNode( DOMText $node ): bool {
    $parent = $node->parentNode;

    while ( $parent instanceof DOMNode ) {
      if ( $parent instanceof DOMElement ) {
        if ( ! is_string( $parent->tagName ) ) {
          return true;
        }

        $tag = strtolower( $parent->tagName );

        if ( in_array( $tag, self::SKIP_TEXT_INSIDE_TAGS, true ) ) {
          return true;
        }

        if ( self::elementHasSkipClass( $parent ) ) {
          return true;
        }

        if ( $parent->hasAttribute( 'contenteditable' ) ) {
          return true;
        }
      }

      $parent = $parent->parentNode;
    }

    return false;
  }

  private static function elementHasSkipClass( DOMElement $element ): bool {
    if ( ! $element->hasAttribute( 'class' ) ) {
      return false;
    }

    $class_attr = $element->getAttribute( 'class' );
    if ( $class_attr === '' ) {
      return false;
    }

    $classes = preg_split( '/\s+/u', trim( $class_attr ) );
    if ( ! is_array( $classes ) || empty( $classes ) ) {
      return false;
    }

    foreach ( $classes as $class ) {
      if ( in_array( $class, self::SKIP_CLASSES, true ) ) {
        return true;
      }
    }

    return false;
  }

  /**
   * Convert only standalone numeric tokens, not digits inside identifiers.
   *
   * Converts:
   *   875254
   *   12.5
   *   +15
   *   -7.2
   *
   * Does not convert:
   *   abc123
   *   SR7_1_1
   *   item_25
   *   v2ray
   */
  private static function convertNumbersInText( string $text ): string {
    return (string) preg_replace_callback(
      '/(?<![\pL\pN_])([+-]?\d+(?:\.\d+)?)(?![\pL\pN_])/u',
      static function ( array $matches ): string {
        return self::toPersianNumber( $matches[1] );
      },
      $text
    );
  }

  private static function toPersianNumber( string $value ): string {
    return strtr(
      $value,
      [
        '0' => '۰',
        '1' => '۱',
        '2' => '۲',
        '3' => '۳',
        '4' => '۴',
        '5' => '۵',
        '6' => '۶',
        '7' => '۷',
        '8' => '۸',
        '9' => '۹',
        // If you want Persian decimal separator instead of ".", uncomment:
        // '.' => '٫',
      ]
    );
  }
}