run:R W Run
3.77 KB
2026-03-11 16:18:52
R W Run
9.32 KB
2026-03-11 16:18:52
R W Run
7.46 KB
2026-03-11 16:18:52
R W Run
15 KB
2026-03-11 16:18:52
R W Run
7.75 KB
2026-03-11 16:18:52
R W Run
12.51 KB
2026-03-11 16:18:52
R W Run
error_log
📄plural-forms.php
1<?php
2
3/**
4 * A gettext Plural-Forms parser.
5 *
6 * @since 4.9.0
7 */
8if ( ! class_exists( 'Plural_Forms', false ) ) :
9 #[AllowDynamicProperties]
10 class Plural_Forms {
11 /**
12 * Operator characters.
13 *
14 * @since 4.9.0
15 * @var string OP_CHARS Operator characters.
16 */
17 const OP_CHARS = '|&><!=%?:';
18
19 /**
20 * Valid number characters.
21 *
22 * @since 4.9.0
23 * @var string NUM_CHARS Valid number characters.
24 */
25 const NUM_CHARS = '0123456789';
26
27 /**
28 * Operator precedence.
29 *
30 * Operator precedence from highest to lowest. Higher numbers indicate
31 * higher precedence, and are executed first.
32 *
33 * @see https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence
34 *
35 * @since 4.9.0
36 * @var array $op_precedence Operator precedence from highest to lowest.
37 */
38 protected static $op_precedence = array(
39 '%' => 6,
40
41 '<' => 5,
42 '<=' => 5,
43 '>' => 5,
44 '>=' => 5,
45
46 '==' => 4,
47 '!=' => 4,
48
49 '&&' => 3,
50
51 '||' => 2,
52
53 '?:' => 1,
54 '?' => 1,
55
56 '(' => 0,
57 ')' => 0,
58 );
59
60 /**
61 * Tokens generated from the string.
62 *
63 * @since 4.9.0
64 * @var array $tokens List of tokens.
65 */
66 protected $tokens = array();
67
68 /**
69 * Cache for repeated calls to the function.
70 *
71 * @since 4.9.0
72 * @var array $cache Map of $n => $result
73 */
74 protected $cache = array();
75
76 /**
77 * Constructor.
78 *
79 * @since 4.9.0
80 *
81 * @param string $str Plural function (just the bit after `plural=` from Plural-Forms)
82 */
83 public function __construct( $str ) {
84 $this->parse( $str );
85 }
86
87 /**
88 * Parse a Plural-Forms string into tokens.
89 *
90 * Uses the shunting-yard algorithm to convert the string to Reverse Polish
91 * Notation tokens.
92 *
93 * @since 4.9.0
94 *
95 * @throws Exception If there is a syntax or parsing error with the string.
96 *
97 * @param string $str String to parse.
98 */
99 protected function parse( $str ) {
100 $pos = 0;
101 $len = strlen( $str );
102
103 // Convert infix operators to postfix using the shunting-yard algorithm.
104 $output = array();
105 $stack = array();
106 while ( $pos < $len ) {
107 $next = substr( $str, $pos, 1 );
108
109 switch ( $next ) {
110 // Ignore whitespace.
111 case ' ':
112 case "\t":
113 ++$pos;
114 break;
115
116 // Variable (n).
117 case 'n':
118 $output[] = array( 'var' );
119 ++$pos;
120 break;
121
122 // Parentheses.
123 case '(':
124 $stack[] = $next;
125 ++$pos;
126 break;
127
128 case ')':
129 $found = false;
130 while ( ! empty( $stack ) ) {
131 $o2 = $stack[ count( $stack ) - 1 ];
132 if ( '(' !== $o2 ) {
133 $output[] = array( 'op', array_pop( $stack ) );
134 continue;
135 }
136
137 // Discard open paren.
138 array_pop( $stack );
139 $found = true;
140 break;
141 }
142
143 if ( ! $found ) {
144 throw new Exception( 'Mismatched parentheses' );
145 }
146
147 ++$pos;
148 break;
149
150 // Operators.
151 case '|':
152 case '&':
153 case '>':
154 case '<':
155 case '!':
156 case '=':
157 case '%':
158 case '?':
159 $end_operator = strspn( $str, self::OP_CHARS, $pos );
160 $operator = substr( $str, $pos, $end_operator );
161 if ( ! array_key_exists( $operator, self::$op_precedence ) ) {
162 throw new Exception( sprintf( 'Unknown operator "%s"', $operator ) );
163 }
164
165 while ( ! empty( $stack ) ) {
166 $o2 = $stack[ count( $stack ) - 1 ];
167
168 // Ternary is right-associative in C.
169 if ( '?:' === $operator || '?' === $operator ) {
170 if ( self::$op_precedence[ $operator ] >= self::$op_precedence[ $o2 ] ) {
171 break;
172 }
173 } elseif ( self::$op_precedence[ $operator ] > self::$op_precedence[ $o2 ] ) {
174 break;
175 }
176
177 $output[] = array( 'op', array_pop( $stack ) );
178 }
179 $stack[] = $operator;
180
181 $pos += $end_operator;
182 break;
183
184 // Ternary "else".
185 case ':':
186 $found = false;
187 $s_pos = count( $stack ) - 1;
188 while ( $s_pos >= 0 ) {
189 $o2 = $stack[ $s_pos ];
190 if ( '?' !== $o2 ) {
191 $output[] = array( 'op', array_pop( $stack ) );
192 --$s_pos;
193 continue;
194 }
195
196 // Replace.
197 $stack[ $s_pos ] = '?:';
198 $found = true;
199 break;
200 }
201
202 if ( ! $found ) {
203 throw new Exception( 'Missing starting "?" ternary operator' );
204 }
205 ++$pos;
206 break;
207
208 // Default - number or invalid.
209 default:
210 if ( $next >= '0' && $next <= '9' ) {
211 $span = strspn( $str, self::NUM_CHARS, $pos );
212 $output[] = array( 'value', intval( substr( $str, $pos, $span ) ) );
213 $pos += $span;
214 break;
215 }
216
217 throw new Exception( sprintf( 'Unknown symbol "%s"', $next ) );
218 }
219 }
220
221 while ( ! empty( $stack ) ) {
222 $o2 = array_pop( $stack );
223 if ( '(' === $o2 || ')' === $o2 ) {
224 throw new Exception( 'Mismatched parentheses' );
225 }
226
227 $output[] = array( 'op', $o2 );
228 }
229
230 $this->tokens = $output;
231 }
232
233 /**
234 * Get the plural form for a number.
235 *
236 * Caches the value for repeated calls.
237 *
238 * @since 4.9.0
239 *
240 * @param int $num Number to get plural form for.
241 * @return int Plural form value.
242 */
243 public function get( $num ) {
244 if ( isset( $this->cache[ $num ] ) ) {
245 return $this->cache[ $num ];
246 }
247 $this->cache[ $num ] = $this->execute( $num );
248 return $this->cache[ $num ];
249 }
250
251 /**
252 * Execute the plural form function.
253 *
254 * @since 4.9.0
255 *
256 * @throws Exception If the plural form value cannot be calculated.
257 *
258 * @param int $n Variable "n" to substitute.
259 * @return int Plural form value.
260 */
261 public function execute( $n ) {
262 $stack = array();
263 $i = 0;
264 $total = count( $this->tokens );
265 while ( $i < $total ) {
266 $next = $this->tokens[ $i ];
267 ++$i;
268 if ( 'var' === $next[0] ) {
269 $stack[] = $n;
270 continue;
271 } elseif ( 'value' === $next[0] ) {
272 $stack[] = $next[1];
273 continue;
274 }
275
276 // Only operators left.
277 switch ( $next[1] ) {
278 case '%':
279 $v2 = array_pop( $stack );
280 $v1 = array_pop( $stack );
281 $stack[] = $v1 % $v2;
282 break;
283
284 case '||':
285 $v2 = array_pop( $stack );
286 $v1 = array_pop( $stack );
287 $stack[] = $v1 || $v2;
288 break;
289
290 case '&&':
291 $v2 = array_pop( $stack );
292 $v1 = array_pop( $stack );
293 $stack[] = $v1 && $v2;
294 break;
295
296 case '<':
297 $v2 = array_pop( $stack );
298 $v1 = array_pop( $stack );
299 $stack[] = $v1 < $v2;
300 break;
301
302 case '<=':
303 $v2 = array_pop( $stack );
304 $v1 = array_pop( $stack );
305 $stack[] = $v1 <= $v2;
306 break;
307
308 case '>':
309 $v2 = array_pop( $stack );
310 $v1 = array_pop( $stack );
311 $stack[] = $v1 > $v2;
312 break;
313
314 case '>=':
315 $v2 = array_pop( $stack );
316 $v1 = array_pop( $stack );
317 $stack[] = $v1 >= $v2;
318 break;
319
320 case '!=':
321 $v2 = array_pop( $stack );
322 $v1 = array_pop( $stack );
323 $stack[] = $v1 !== $v2;
324 break;
325
326 case '==':
327 $v2 = array_pop( $stack );
328 $v1 = array_pop( $stack );
329 $stack[] = $v1 === $v2;
330 break;
331
332 case '?:':
333 $v3 = array_pop( $stack );
334 $v2 = array_pop( $stack );
335 $v1 = array_pop( $stack );
336 $stack[] = $v1 ? $v2 : $v3;
337 break;
338
339 default:
340 throw new Exception( sprintf( 'Unknown operator "%s"', $next[1] ) );
341 }
342 }
343
344 if ( count( $stack ) !== 1 ) {
345 throw new Exception( 'Too many values remaining on the stack' );
346 }
347
348 return (int) $stack[0];
349 }
350 }
351endif;
352