352 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| /**
 | |
|  * A gettext Plural-Forms parser.
 | |
|  *
 | |
|  * @since 4.9.0
 | |
|  */
 | |
| if ( ! class_exists( 'Plural_Forms', false ) ) :
 | |
| 	#[AllowDynamicProperties]
 | |
| 	class Plural_Forms {
 | |
| 		/**
 | |
| 		 * Operator characters.
 | |
| 		 *
 | |
| 		 * @since 4.9.0
 | |
| 		 * @var string OP_CHARS Operator characters.
 | |
| 		 */
 | |
| 		const OP_CHARS = '|&><!=%?:';
 | |
| 
 | |
| 		/**
 | |
| 		 * Valid number characters.
 | |
| 		 *
 | |
| 		 * @since 4.9.0
 | |
| 		 * @var string NUM_CHARS Valid number characters.
 | |
| 		 */
 | |
| 		const NUM_CHARS = '0123456789';
 | |
| 
 | |
| 		/**
 | |
| 		 * Operator precedence.
 | |
| 		 *
 | |
| 		 * Operator precedence from highest to lowest. Higher numbers indicate
 | |
| 		 * higher precedence, and are executed first.
 | |
| 		 *
 | |
| 		 * @see https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence
 | |
| 		 *
 | |
| 		 * @since 4.9.0
 | |
| 		 * @var array $op_precedence Operator precedence from highest to lowest.
 | |
| 		 */
 | |
| 		protected static $op_precedence = array(
 | |
| 			'%'  => 6,
 | |
| 
 | |
| 			'<'  => 5,
 | |
| 			'<=' => 5,
 | |
| 			'>'  => 5,
 | |
| 			'>=' => 5,
 | |
| 
 | |
| 			'==' => 4,
 | |
| 			'!=' => 4,
 | |
| 
 | |
| 			'&&' => 3,
 | |
| 
 | |
| 			'||' => 2,
 | |
| 
 | |
| 			'?:' => 1,
 | |
| 			'?'  => 1,
 | |
| 
 | |
| 			'('  => 0,
 | |
| 			')'  => 0,
 | |
| 		);
 | |
| 
 | |
| 		/**
 | |
| 		 * Tokens generated from the string.
 | |
| 		 *
 | |
| 		 * @since 4.9.0
 | |
| 		 * @var array $tokens List of tokens.
 | |
| 		 */
 | |
| 		protected $tokens = array();
 | |
| 
 | |
| 		/**
 | |
| 		 * Cache for repeated calls to the function.
 | |
| 		 *
 | |
| 		 * @since 4.9.0
 | |
| 		 * @var array $cache Map of $n => $result
 | |
| 		 */
 | |
| 		protected $cache = array();
 | |
| 
 | |
| 		/**
 | |
| 		 * Constructor.
 | |
| 		 *
 | |
| 		 * @since 4.9.0
 | |
| 		 *
 | |
| 		 * @param string $str Plural function (just the bit after `plural=` from Plural-Forms)
 | |
| 		 */
 | |
| 		public function __construct( $str ) {
 | |
| 			$this->parse( $str );
 | |
| 		}
 | |
| 
 | |
| 		/**
 | |
| 		 * Parse a Plural-Forms string into tokens.
 | |
| 		 *
 | |
| 		 * Uses the shunting-yard algorithm to convert the string to Reverse Polish
 | |
| 		 * Notation tokens.
 | |
| 		 *
 | |
| 		 * @since 4.9.0
 | |
| 		 *
 | |
| 		 * @throws Exception If there is a syntax or parsing error with the string.
 | |
| 		 *
 | |
| 		 * @param string $str String to parse.
 | |
| 		 */
 | |
| 		protected function parse( $str ) {
 | |
| 			$pos = 0;
 | |
| 			$len = strlen( $str );
 | |
| 
 | |
| 			// Convert infix operators to postfix using the shunting-yard algorithm.
 | |
| 			$output = array();
 | |
| 			$stack  = array();
 | |
| 			while ( $pos < $len ) {
 | |
| 				$next = substr( $str, $pos, 1 );
 | |
| 
 | |
| 				switch ( $next ) {
 | |
| 					// Ignore whitespace.
 | |
| 					case ' ':
 | |
| 					case "\t":
 | |
| 						++$pos;
 | |
| 						break;
 | |
| 
 | |
| 					// Variable (n).
 | |
| 					case 'n':
 | |
| 						$output[] = array( 'var' );
 | |
| 						++$pos;
 | |
| 						break;
 | |
| 
 | |
| 					// Parentheses.
 | |
| 					case '(':
 | |
| 						$stack[] = $next;
 | |
| 						++$pos;
 | |
| 						break;
 | |
| 
 | |
| 					case ')':
 | |
| 						$found = false;
 | |
| 						while ( ! empty( $stack ) ) {
 | |
| 							$o2 = $stack[ count( $stack ) - 1 ];
 | |
| 							if ( '(' !== $o2 ) {
 | |
| 								$output[] = array( 'op', array_pop( $stack ) );
 | |
| 								continue;
 | |
| 							}
 | |
| 
 | |
| 							// Discard open paren.
 | |
| 							array_pop( $stack );
 | |
| 							$found = true;
 | |
| 							break;
 | |
| 						}
 | |
| 
 | |
| 						if ( ! $found ) {
 | |
| 							throw new Exception( 'Mismatched parentheses' );
 | |
| 						}
 | |
| 
 | |
| 						++$pos;
 | |
| 						break;
 | |
| 
 | |
| 					// Operators.
 | |
| 					case '|':
 | |
| 					case '&':
 | |
| 					case '>':
 | |
| 					case '<':
 | |
| 					case '!':
 | |
| 					case '=':
 | |
| 					case '%':
 | |
| 					case '?':
 | |
| 						$end_operator = strspn( $str, self::OP_CHARS, $pos );
 | |
| 						$operator     = substr( $str, $pos, $end_operator );
 | |
| 						if ( ! array_key_exists( $operator, self::$op_precedence ) ) {
 | |
| 							throw new Exception( sprintf( 'Unknown operator "%s"', $operator ) );
 | |
| 						}
 | |
| 
 | |
| 						while ( ! empty( $stack ) ) {
 | |
| 							$o2 = $stack[ count( $stack ) - 1 ];
 | |
| 
 | |
| 							// Ternary is right-associative in C.
 | |
| 							if ( '?:' === $operator || '?' === $operator ) {
 | |
| 								if ( self::$op_precedence[ $operator ] >= self::$op_precedence[ $o2 ] ) {
 | |
| 									break;
 | |
| 								}
 | |
| 							} elseif ( self::$op_precedence[ $operator ] > self::$op_precedence[ $o2 ] ) {
 | |
| 								break;
 | |
| 							}
 | |
| 
 | |
| 							$output[] = array( 'op', array_pop( $stack ) );
 | |
| 						}
 | |
| 						$stack[] = $operator;
 | |
| 
 | |
| 						$pos += $end_operator;
 | |
| 						break;
 | |
| 
 | |
| 					// Ternary "else".
 | |
| 					case ':':
 | |
| 						$found = false;
 | |
| 						$s_pos = count( $stack ) - 1;
 | |
| 						while ( $s_pos >= 0 ) {
 | |
| 							$o2 = $stack[ $s_pos ];
 | |
| 							if ( '?' !== $o2 ) {
 | |
| 								$output[] = array( 'op', array_pop( $stack ) );
 | |
| 								--$s_pos;
 | |
| 								continue;
 | |
| 							}
 | |
| 
 | |
| 							// Replace.
 | |
| 							$stack[ $s_pos ] = '?:';
 | |
| 							$found           = true;
 | |
| 							break;
 | |
| 						}
 | |
| 
 | |
| 						if ( ! $found ) {
 | |
| 							throw new Exception( 'Missing starting "?" ternary operator' );
 | |
| 						}
 | |
| 						++$pos;
 | |
| 						break;
 | |
| 
 | |
| 					// Default - number or invalid.
 | |
| 					default:
 | |
| 						if ( $next >= '0' && $next <= '9' ) {
 | |
| 							$span     = strspn( $str, self::NUM_CHARS, $pos );
 | |
| 							$output[] = array( 'value', intval( substr( $str, $pos, $span ) ) );
 | |
| 							$pos     += $span;
 | |
| 							break;
 | |
| 						}
 | |
| 
 | |
| 						throw new Exception( sprintf( 'Unknown symbol "%s"', $next ) );
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			while ( ! empty( $stack ) ) {
 | |
| 				$o2 = array_pop( $stack );
 | |
| 				if ( '(' === $o2 || ')' === $o2 ) {
 | |
| 					throw new Exception( 'Mismatched parentheses' );
 | |
| 				}
 | |
| 
 | |
| 				$output[] = array( 'op', $o2 );
 | |
| 			}
 | |
| 
 | |
| 			$this->tokens = $output;
 | |
| 		}
 | |
| 
 | |
| 		/**
 | |
| 		 * Get the plural form for a number.
 | |
| 		 *
 | |
| 		 * Caches the value for repeated calls.
 | |
| 		 *
 | |
| 		 * @since 4.9.0
 | |
| 		 *
 | |
| 		 * @param int $num Number to get plural form for.
 | |
| 		 * @return int Plural form value.
 | |
| 		 */
 | |
| 		public function get( $num ) {
 | |
| 			if ( isset( $this->cache[ $num ] ) ) {
 | |
| 				return $this->cache[ $num ];
 | |
| 			}
 | |
| 			$this->cache[ $num ] = $this->execute( $num );
 | |
| 			return $this->cache[ $num ];
 | |
| 		}
 | |
| 
 | |
| 		/**
 | |
| 		 * Execute the plural form function.
 | |
| 		 *
 | |
| 		 * @since 4.9.0
 | |
| 		 *
 | |
| 		 * @throws Exception If the plural form value cannot be calculated.
 | |
| 		 *
 | |
| 		 * @param int $n Variable "n" to substitute.
 | |
| 		 * @return int Plural form value.
 | |
| 		 */
 | |
| 		public function execute( $n ) {
 | |
| 			$stack = array();
 | |
| 			$i     = 0;
 | |
| 			$total = count( $this->tokens );
 | |
| 			while ( $i < $total ) {
 | |
| 				$next = $this->tokens[ $i ];
 | |
| 				++$i;
 | |
| 				if ( 'var' === $next[0] ) {
 | |
| 					$stack[] = $n;
 | |
| 					continue;
 | |
| 				} elseif ( 'value' === $next[0] ) {
 | |
| 					$stack[] = $next[1];
 | |
| 					continue;
 | |
| 				}
 | |
| 
 | |
| 				// Only operators left.
 | |
| 				switch ( $next[1] ) {
 | |
| 					case '%':
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 % $v2;
 | |
| 						break;
 | |
| 
 | |
| 					case '||':
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 || $v2;
 | |
| 						break;
 | |
| 
 | |
| 					case '&&':
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 && $v2;
 | |
| 						break;
 | |
| 
 | |
| 					case '<':
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 < $v2;
 | |
| 						break;
 | |
| 
 | |
| 					case '<=':
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 <= $v2;
 | |
| 						break;
 | |
| 
 | |
| 					case '>':
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 > $v2;
 | |
| 						break;
 | |
| 
 | |
| 					case '>=':
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 >= $v2;
 | |
| 						break;
 | |
| 
 | |
| 					case '!=':
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 != $v2;
 | |
| 						break;
 | |
| 
 | |
| 					case '==':
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 == $v2;
 | |
| 						break;
 | |
| 
 | |
| 					case '?:':
 | |
| 						$v3      = array_pop( $stack );
 | |
| 						$v2      = array_pop( $stack );
 | |
| 						$v1      = array_pop( $stack );
 | |
| 						$stack[] = $v1 ? $v2 : $v3;
 | |
| 						break;
 | |
| 
 | |
| 					default:
 | |
| 						throw new Exception( sprintf( 'Unknown operator "%s"', $next[1] ) );
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if ( count( $stack ) !== 1 ) {
 | |
| 				throw new Exception( 'Too many values remaining on the stack' );
 | |
| 			}
 | |
| 
 | |
| 			return (int) $stack[0];
 | |
| 		}
 | |
| 	}
 | |
| endif;
 |