Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
41.75% |
119 / 285 |
|
35.71% |
5 / 14 |
CRAP | |
0.00% |
0 / 1 |
ChessBrowser | |
41.75% |
119 / 285 |
|
35.71% |
5 / 14 |
608.06 | |
0.00% |
0 / 1 |
newGame | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
assertValidPGN | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
3 | |||
decideResultString | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
createBoard | |
96.77% |
30 / 31 |
|
0.00% |
0 / 1 |
4 | |||
newPosition | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
6 | |||
assertValidFEN | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
parseArguments | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
5.01 | |||
generatePieces | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
getLocalizedLabels | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
getLocalizedLegendLabels | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
6 | |||
getMoveSet | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
42 | |||
getVariationSet | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
getMetadata | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
3 | |||
createPiece | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
7 |
1 | <?php |
2 | /** |
3 | * This file is a part of ChessBrowser. |
4 | * |
5 | * ChessBrowser is free software: you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation, either version 3 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License |
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
17 | * |
18 | * @file ChessBrowser |
19 | * @ingroup ChessBrowser |
20 | * @author Wugapodes |
21 | */ |
22 | |
23 | namespace MediaWiki\Extension\ChessBrowser; |
24 | |
25 | use Exception; |
26 | use MediaWiki\Extension\ChessBrowser\PgnParser\FenParser0x88; |
27 | use MediaWiki\Html\TemplateParser; |
28 | use MediaWiki\Parser\Parser; |
29 | use MediaWiki\Parser\PPFrame; |
30 | |
31 | class ChessBrowser { |
32 | /** |
33 | * @since 0.1.0 |
34 | * @param string $input The wikitext placed between pgn tags |
35 | * @param array $args Arguments passed as xml attributes |
36 | * @param Parser $parser The MediaWiki parser object |
37 | * @param PPFrame $frame Parent frame, provides context of the tage placement |
38 | * @return array |
39 | */ |
40 | public static function newGame( $input, array $args, Parser $parser, PPFrame $frame ): array { |
41 | try { |
42 | self::assertValidPGN( $input ); |
43 | |
44 | $out = $parser->getOutput(); |
45 | // Get number of games so div id property is unique |
46 | $gameNum = $out->getExtensionData( 'ChessViewerNumGames' ) ?? 0; |
47 | $gameNum++; |
48 | |
49 | $board = self::createBoard( $input, $gameNum, $args ); |
50 | |
51 | // Set after the parsing, etc. in case there is an error |
52 | // Set variable so resource loader knows whether to send javascript |
53 | $out->setExtensionData( 'ChessViewerTrigger', 'true' ); |
54 | // Increment number of games |
55 | $out->setExtensionData( 'ChessViewerNumGames', $gameNum ); |
56 | |
57 | return [ $board, "markerType" => "nowiki" ]; |
58 | } catch ( Exception $e ) { |
59 | wfDebugLog( |
60 | 'ChessBrowser', |
61 | 'Unable to create a game: ' . $e |
62 | ); |
63 | $parser->addTrackingCategory( 'chessbrowser-invalid-category' ); |
64 | $message = wfMessage( 'chessbrowser-invalid-message' )->escaped(); |
65 | return [ $message ]; |
66 | } |
67 | } |
68 | |
69 | /** |
70 | * Check if tag cotains valid input format PGN |
71 | * |
72 | * The input PGN is checked by various regular expressions to get a rough |
73 | * sense of whether the PGN is valid before devoting the parsing resources. |
74 | * |
75 | * The validation is a series of filters which remove sections that the |
76 | * parser can understand. The PGN is valid if the entire input gets |
77 | * filtered and is invalid if there is some part of the string that |
78 | * remains after the filtering. |
79 | * |
80 | * @param string $input |
81 | * @throws ChessBrowserException if invalid |
82 | */ |
83 | private static function assertValidPGN( string $input ) { |
84 | // Validate escaped lines (PGN Standard 6) identified by a % sign at |
85 | // start of line and ignoring all following text on the line. A % |
86 | // anywhere else but the start of line has no meaning. |
87 | $escapedLine = '/^%.*?\n/m'; |
88 | $input = preg_replace( $escapedLine, "", $input ); |
89 | |
90 | // Validate tag pairs (PGN Standard 8.1). Composed of four tokens: |
91 | // left bracket, ie: [ |
92 | // symbol token, a sequence comprising only alphanumeric chars and underscore |
93 | // string token, a symbol token delimited by double quotes |
94 | // right bracket, ie: ] |
95 | // Input format allows any ammount of whitespace to separate these tokens. |
96 | $tagPairs = '/\[\s*[a-zA-Z0-9_]+\s*".*"\s*\]/'; |
97 | $input = preg_replace( $tagPairs, "", $input ); |
98 | |
99 | // Validate comments (PGN Standard 5) and move variations (idem 8.2.5). |
100 | // Inline comments are delimited by braces |
101 | // Rest-of-line comments are delimited by ; and a newline, EOL, or EOF |
102 | // Variations are delimited by parentheses |
103 | // |
104 | // Single line or else multi-line comments fail, see T370499 |
105 | $annotations = '/({.*?}|\(.*?\)|;.*?(\n|$|\Z))/s'; |
106 | $input = preg_replace( $annotations, "", $input ); |
107 | |
108 | // Validate Numeric Annotation Glyphs (NAGs; PGN Standard 10). Composed of: |
109 | // a leading dollar sign ($) |
110 | // a non-negative decimal integer between 0 and 255 |
111 | // We do not check that the NAG is proper, only that it has the same number of |
112 | // digits as a proper NAG, i.e., not greater than a thousand. |
113 | $NAGs = '/\$\d{1,3}/'; |
114 | $input = preg_replace( $NAGs, "", $input ); |
115 | |
116 | // Validate game termination markers (PGN Standard 8.2.6). One of four values: |
117 | // 1-0 |
118 | // 0-1 |
119 | // 1/2-1/2 |
120 | // * |
121 | // We allow a great deal of leeway in the separator accepting any non-alphanumeric |
122 | // value (\W). This is to accomodate en- or em-dashes, periods, spaces, etc |
123 | // without a complex pre-processing overhead. |
124 | // The game will not validate if there is more than one termination marker. |
125 | $termCount = 0; |
126 | $limit = -1; |
127 | $termination = '/(1\W0|0\W1|1\/2\W1\/2|\*)/'; |
128 | $input = preg_replace( $termination, "", $input, $limit, $termCount ); |
129 | if ( $termCount > 1 ) { |
130 | throw new ChessBrowserException( 'Too many termination tokens.' ); |
131 | } |
132 | |
133 | // Validate move number indicators (PGN Standard 8.2.2) if they exist. Move |
134 | // numbers are composed of: |
135 | // a leading non-alphanumeric character (\W) which terminates the previous token |
136 | // one or more digits (\d+) |
137 | // any amount of whitespace (\s*) |
138 | // any number of periods (\.*) |
139 | // Input PGN format is extremely forgiving with regards to the last two criteria |
140 | $moveNumbers = '/\W\d+\s*\.*/'; |
141 | // Make sure SAN starts with a space so that first move number is matched |
142 | $input = " " . $input; |
143 | $input = preg_replace( $moveNumbers, "", $input ); |
144 | |
145 | // Validate standard algebraic notation (SAN; PGN Standard 8.2.3). As these denote |
146 | // moves on a chess board, their composition is restricted to: |
147 | // files (a-h) |
148 | // ranks (1-8) |
149 | // the capture indicator (x) |
150 | // piece indicators (BNRKQ) |
151 | // promotion indictor (=) |
152 | // check (+) and checkmate(#) |
153 | // components of a castling marker (O-O and O-O-O) |
154 | // The minimum length of a SAN token is 2 characters, denoting a pawn move. |
155 | // The theoretical maximum is 7, but we do not check the upper bound. |
156 | $SAN = '/[a-hxOBNRKQ1-8=+#\-]{2,}/'; |
157 | $input = preg_replace( $SAN, "", $input ); |
158 | |
159 | // Moves may be annotated by some number of glyphs. Check all known by NagTable. |
160 | $glyphs = array_values( NagTable::MAP ); |
161 | $input = str_replace( $glyphs, "", $input ); |
162 | |
163 | // If the PGN is valid, we should have either an empty string or a string containing |
164 | // only white space. If, after removing the white space, we have anything left in |
165 | // the string, we know that the PGN is not valid. |
166 | $whitespace = '/\s+/'; |
167 | $input = preg_replace( $whitespace, "", $input ); |
168 | |
169 | if ( strlen( $input ) > 0 ) { |
170 | throw new ChessBrowserException( 'Invalid PGN.' ); |
171 | } |
172 | } |
173 | |
174 | /** |
175 | * Check tag pair result against notation-end result |
176 | * |
177 | * @param string $input The text inside the pgn tag |
178 | * @param array $metadata Dictionary of tag pair key-values |
179 | * @return string |
180 | * @throws ChessBrowserException if Tag Pair result and movetext termination string are both valid but don't match. |
181 | */ |
182 | private static function decideResultString( string $input, array $metadata ): string { |
183 | $terminationRegex = '/(1\W0|0\W1|1\/2\W1\/2|\*)/'; |
184 | $tagResult = $metadata['result']; |
185 | $validTagPairResult = ( preg_match( $terminationRegex, $tagResult, $tagResultToken ) === 1 ); |
186 | // Termination markers can appear all over the place in the input text like |
187 | // in variations or comments, so preg_match can have false positives. The real |
188 | // term marker should be at the end of the game, so checking the last 7 chars |
189 | // (the longest term marker of 1/2-1/2) should avoid false positives. |
190 | $endOfMovetext = substr( $input, -7 ); |
191 | $validMovetextTerm = ( preg_match( $terminationRegex, $endOfMovetext, $movetextTermToken ) === 1 ); |
192 | |
193 | if ( $validTagPairResult && $validMovetextTerm ) { |
194 | // Check that they match |
195 | if ( strcmp( $tagResultToken[1], $movetextTermToken[1] ) !== 0 ) { |
196 | $errorMsg = 'Invalid PGN: Result tag and game termination marker do not match.'; |
197 | throw new ChessBrowserException( $errorMsg ); |
198 | } |
199 | // Go with the movetext token |
200 | return $movetextTermToken[1]; |
201 | } elseif ( $validTagPairResult ) { |
202 | return $tagResultToken[1]; |
203 | } elseif ( $validMovetextTerm ) { |
204 | return $movetextTermToken[1]; |
205 | } else { |
206 | // Don't fail, just print nothing, TODO add tracking cat |
207 | return ''; |
208 | } |
209 | } |
210 | |
211 | /** |
212 | * Handle creating the board to show |
213 | * |
214 | * @param string $input |
215 | * @param int $gameNum |
216 | * @param array $args The XML arguments from MediaWiki |
217 | * @return string |
218 | * @throws ChessBrowserException |
219 | */ |
220 | private static function createBoard( string $input, int $gameNum, array $args ): string { |
221 | $attr = self::parseArguments( $args ); |
222 | $swap = $attr['side'] === 'black'; |
223 | $initialPosition = $attr['ply']; |
224 | |
225 | // Initialize parsers |
226 | $chessParser = new ChessParser( $input ); |
227 | |
228 | $chessObject = $chessParser->createOutputJson(); |
229 | $annotationObject = $chessObject['variations']; |
230 | unset( $chessObject['variations'] ); |
231 | $chessObject['init'] = $initialPosition; |
232 | if ( !$chessObject['boards'][0] ) { |
233 | throw new ChessBrowserException( 'No board available' ); |
234 | } |
235 | |
236 | // Set up template arguments |
237 | $templateParser = new TemplateParser( __DIR__ . '/../templates' ); |
238 | $templateParser->enableRecursivePartials( true ); |
239 | $templateArgs = [ |
240 | 'data-chess' => json_encode( $chessObject ), |
241 | 'data-chess-annotations' => json_encode( $annotationObject ), |
242 | 'div-number' => $gameNum, |
243 | // The JS toggles a class to flip games, so unlike FEN we only need |
244 | // to add the class in order to flip the board to black's view. |
245 | // Include notransition class so that readers don't get FOUC and |
246 | // watch all the boards spin on load |
247 | 'swap' => $swap ? ' pgn-flip notransition' : '', |
248 | 'move-set' => self::getMoveSet( $chessObject, $annotationObject ), |
249 | 'piece-set' => self::generatePieces( $chessObject['boards'][0] ) |
250 | ]; |
251 | $localizedLabels = self::getLocalizedLabels(); |
252 | $metadata = self::getMetadata( $chessObject['metadata'] ); |
253 | $resultString = self::decideResultString( $input, $metadata ); |
254 | if ( strcmp( $resultString, '' ) !== 0 ) { |
255 | $templateArgs['movetext-result'] = $resultString; |
256 | } |
257 | $templateArgs = array_merge( $templateArgs, $localizedLabels, $metadata ); |
258 | $game = $templateParser->processTemplate( |
259 | 'ChessGame', |
260 | $templateArgs |
261 | ); |
262 | return $game; |
263 | } |
264 | |
265 | /** |
266 | * @since 0.3.0 |
267 | * @param string $input The wikitext placed between fen tags |
268 | * @param array $args Arguments passed as xml attributes |
269 | * @param Parser $parser The MediaWiki parser object |
270 | * @param PPFrame $frame Parent frame, provides context of the tage placement |
271 | * @return array |
272 | */ |
273 | public static function newPosition( string $input, array $args, Parser $parser, PPFrame $frame ): array { |
274 | try { |
275 | $attr = self::parseArguments( $args ); |
276 | $swap = $attr['side'] === 'black'; |
277 | |
278 | $input = trim( $input ); |
279 | self::assertValidFEN( $input ); |
280 | $fenParser = new FenParser0x88( $input ); |
281 | $fenOut = $fenParser->getFen(); |
282 | |
283 | // Set up template arguments |
284 | $templateParser = new TemplateParser( __DIR__ . '/../templates' ); |
285 | $templateArgs = [ |
286 | 'data-chess' => json_encode( $fenOut ), |
287 | 'piece-set' => self::generatePieces( $fenOut, $swap ) |
288 | ]; |
289 | $localizedLegendLabels = self::getLocalizedLegendLabels( $swap ); |
290 | $templateArgs = array_merge( $templateArgs, $localizedLegendLabels ); |
291 | $board = $templateParser->processTemplate( |
292 | 'ChessBoard', |
293 | $templateArgs |
294 | ); |
295 | $parser->getOutput()->setExtensionData( 'ChessViewerFEN', 'true' ); |
296 | return [ $board, 'markerType' => 'nowiki' ]; |
297 | } catch ( Exception $e ) { |
298 | wfDebugLog( |
299 | 'ChessBrowser', |
300 | 'Unable to create a game: ' . $e |
301 | ); |
302 | $parser->addTrackingCategory( 'chessbrowser-invalid-category' ); |
303 | $message = wfMessage( 'chessbrowser-invalid-message' )->escaped(); |
304 | return [ $message ]; |
305 | } |
306 | } |
307 | |
308 | /** |
309 | * Check if tag contains valid input format FEN |
310 | * |
311 | * The input string is checked with a regex to make sure we only have the expected |
312 | * characters and spacing of FEN. We do not check if it is a valid game. |
313 | * |
314 | * @param string $fenInput |
315 | * @throws ChessBrowserException if invalid |
316 | */ |
317 | private static function assertValidFEN( string $fenInput ) { |
318 | $fenRegex = '/^([prnbqk1-8]{1,8}\/){7}[prnbqk1-8]{1,8}\s[wb]\s([kq]{1,4}|-)\s([abcdefgh][36]|-)\s\d+\s\d+/i'; |
319 | $valid = preg_match( $fenRegex, $fenInput ); |
320 | if ( $valid !== 1 ) { |
321 | throw new ChessBrowserException( 'Invalid FEN.' ); |
322 | } |
323 | } |
324 | |
325 | /** |
326 | * Return associative array with argument defaults |
327 | * |
328 | * @param array $args Arguments passed as xml attributes through MediaWiki parser |
329 | * @return array |
330 | */ |
331 | public static function parseArguments( array $args ): array { |
332 | $attr = [ |
333 | 'side' => 'white', |
334 | 'ply' => 1 |
335 | ]; |
336 | foreach ( $args as $name => $value ) { |
337 | if ( array_key_exists( $name, $attr ) ) { |
338 | $attr[$name] = $value; |
339 | } |
340 | } |
341 | if ( !in_array( $attr['side'], [ 'white', 'black' ] ) ) { |
342 | $attr['side'] = 'white'; |
343 | } |
344 | // Ensure that an integer is always returned |
345 | $attr['ply'] = (int)$attr['ply']; |
346 | // Setting display to 0 results in the last ply being displayed, not |
347 | // the initial board state which is counterintuitive. Rewrite 0 to 1 |
348 | // to prevent this from happening. |
349 | // TODO: Add some kind of warning about this behavior or fix it in JS |
350 | if ( $attr['ply'] === 0 ) { |
351 | $attr['ply'] = 1; |
352 | } |
353 | return $attr; |
354 | } |
355 | |
356 | /** |
357 | * Create array of mustache arguments for chess-piece.mustache from a given FEN string |
358 | * @since 0.2.0 |
359 | * @param string $fen |
360 | * @param bool $swap Display from black's perspective if true |
361 | * @return array |
362 | */ |
363 | public static function generatePieces( $fen, $swap = false ): array { |
364 | $pieceArray = []; |
365 | $rankIndex = 7; |
366 | $fileIndex = 0; |
367 | $fenArray = str_split( $fen ); |
368 | foreach ( $fenArray as $fenChar ) { |
369 | if ( is_numeric( $fenChar ) ) { |
370 | $fileIndex += $fenChar; |
371 | } elseif ( $fenChar === '/' ) { |
372 | $rankIndex--; |
373 | $fileIndex = 0; |
374 | } else { |
375 | if ( $fileIndex > 7 ) { |
376 | continue; |
377 | } |
378 | if ( $swap ) { |
379 | $piece = self::createPiece( $fenChar, 7 - $rankIndex, $fileIndex ); |
380 | } else { |
381 | $piece = self::createPiece( $fenChar, $rankIndex, $fileIndex ); |
382 | } |
383 | |
384 | $pieceArray[] = $piece; |
385 | $fileIndex++; |
386 | } |
387 | } |
388 | return $pieceArray; |
389 | } |
390 | |
391 | /** |
392 | * Retrieve the interface text for the correct locale |
393 | * @since 0.2.0 |
394 | * @param bool $swap |
395 | * @return array |
396 | */ |
397 | public static function getLocalizedLabels( bool $swap = false ): array { |
398 | $legend = self::getLocalizedLegendLabels( $swap ); |
399 | $other = [ |
400 | 'expand-button' => wfMessage( 'chessbrowser-expand-button' )->text(), |
401 | 'game-detail' => wfMessage( 'chessbrowser-game-detail' )->text(), |
402 | 'event-label' => wfMessage( 'chessbrowser-event-label' )->text(), |
403 | 'site-label' => wfMessage( 'chessbrowser-site-label' )->text(), |
404 | 'date-label' => wfMessage( 'chessbrowser-date-label' )->text(), |
405 | 'round-label' => wfMessage( 'chessbrowser-round-label' )->text(), |
406 | 'white-label' => wfMessage( 'chessbrowser-white-label' )->text(), |
407 | 'black-label' => wfMessage( 'chessbrowser-black-label' )->text(), |
408 | 'result-label' => wfMessage( 'chessbrowser-result-label' )->text(), |
409 | 'notations-label' => wfMessage( 'chessbrowser-notations-label' )->text(), |
410 | 'no-javascript' => wfMessage( 'chessbrowser-no-javascript' )->text() |
411 | ]; |
412 | $allLabels = array_merge( $legend, $other ); |
413 | return $allLabels; |
414 | } |
415 | |
416 | /** |
417 | * Retrieve the interface text for the correct locale for the legend only |
418 | * @since 0.3.0 |
419 | * @param bool $swap |
420 | * @return array |
421 | */ |
422 | private static function getLocalizedLegendLabels( bool $swap ): array { |
423 | if ( $swap ) { |
424 | $ranks = [ |
425 | 'rank-8' => wfMessage( 'chessbrowser-first-rank' )->text(), |
426 | 'rank-7' => wfMessage( 'chessbrowser-second-rank' )->text(), |
427 | 'rank-6' => wfMessage( 'chessbrowser-third-rank' )->text(), |
428 | 'rank-5' => wfMessage( 'chessbrowser-fourth-rank' )->text(), |
429 | 'rank-4' => wfMessage( 'chessbrowser-fifth-rank' )->text(), |
430 | 'rank-3' => wfMessage( 'chessbrowser-sixth-rank' )->text(), |
431 | 'rank-2' => wfMessage( 'chessbrowser-seventh-rank' )->text(), |
432 | 'rank-1' => wfMessage( 'chessbrowser-eighth-rank' )->text(), |
433 | ]; |
434 | } else { |
435 | $ranks = [ |
436 | 'rank-1' => wfMessage( 'chessbrowser-first-rank' )->text(), |
437 | 'rank-2' => wfMessage( 'chessbrowser-second-rank' )->text(), |
438 | 'rank-3' => wfMessage( 'chessbrowser-third-rank' )->text(), |
439 | 'rank-4' => wfMessage( 'chessbrowser-fourth-rank' )->text(), |
440 | 'rank-5' => wfMessage( 'chessbrowser-fifth-rank' )->text(), |
441 | 'rank-6' => wfMessage( 'chessbrowser-sixth-rank' )->text(), |
442 | 'rank-7' => wfMessage( 'chessbrowser-seventh-rank' )->text(), |
443 | 'rank-8' => wfMessage( 'chessbrowser-eighth-rank' )->text(), |
444 | ]; |
445 | } |
446 | $files = [ |
447 | 'a' => wfMessage( 'chessbrowser-a-file' )->text(), |
448 | 'b' => wfMessage( 'chessbrowser-b-file' )->text(), |
449 | 'c' => wfMessage( 'chessbrowser-c-file' )->text(), |
450 | 'd' => wfMessage( 'chessbrowser-d-file' )->text(), |
451 | 'e' => wfMessage( 'chessbrowser-e-file' )->text(), |
452 | 'f' => wfMessage( 'chessbrowser-f-file' )->text(), |
453 | 'g' => wfMessage( 'chessbrowser-g-file' )->text(), |
454 | 'h' => wfMessage( 'chessbrowser-h-file' )->text(), |
455 | ]; |
456 | |
457 | return array_merge( $ranks, $files ); |
458 | } |
459 | |
460 | /** |
461 | * Create array of mustache arguments for move-span.mustache from a given |
462 | * array of ply tokens. |
463 | * @since 0.2.0 |
464 | * @param array $gameObject Game representation loaded into data-chess |
465 | * and output from ChessParser::createOutputJson |
466 | * @param array $annotationObject representation loaded into data-chess-annotations |
467 | * @return array |
468 | */ |
469 | public static function getMoveSet( $gameObject, $annotationObject ): array { |
470 | $tokens = $gameObject['tokens']; |
471 | $plys = $gameObject['plys']; |
472 | $moveSet = []; |
473 | $variationIndices = array_map( |
474 | static function ( $x ) { |
475 | return $x[0]; |
476 | }, |
477 | $annotationObject |
478 | ); |
479 | foreach ( $tokens as $i => $token ) { |
480 | $span = [ |
481 | 'step-link' => false, |
482 | 'annotations' => [] |
483 | ]; |
484 | if ( in_array( $i, $variationIndices ) ) { |
485 | $span['variations'] = []; |
486 | $j = array_search( $i, $variationIndices ); |
487 | $variationList = $annotationObject[$j][1]; |
488 | foreach ( $variationList as $variation ) { |
489 | $span['variations'][] = [ |
490 | 'debug' => json_encode( $variation ), |
491 | 'variation-moves' => self::getVariationSet( $variation, $i ) |
492 | ]; |
493 | // $span['variation-set'][] = self::getVariationSet( $variation, [], $i ); |
494 | } |
495 | } |
496 | $ply = $plys[$i]; |
497 | $comment = $ply[2][2]; |
498 | if ( $comment !== null ) { |
499 | $span['annotations'][] = [ 'comment' => $comment ]; |
500 | } |
501 | if ( $i % 2 === 0 ) { |
502 | $moveNumber = ( $i / 2 ) + 1; |
503 | $span['step-link'] = true; |
504 | $span['move-number'] = $moveNumber; |
505 | } |
506 | $plyNumber = $i + 1; |
507 | $span['move-token'] = $token; |
508 | $span['move-ply'] = $plyNumber; |
509 | $moveSet[] = $span; |
510 | } |
511 | |
512 | return $moveSet; |
513 | } |
514 | |
515 | /** |
516 | * Create template parameters for move variation strings |
517 | * @param array $variation Object listing tokens, boards, and plys for |
518 | * the variation moves. |
519 | * @param int $index The ply of the parent move |
520 | * @return array |
521 | */ |
522 | public static function getVariationSet( $variation, $index ) { |
523 | $tokens = $variation['tokens']; |
524 | $plys = $variation['plys']; |
525 | $spanList = []; |
526 | foreach ( $tokens as $i => $token ) { |
527 | $span = [ |
528 | 'step-link' => false, |
529 | 'annotations' => [] |
530 | ]; |
531 | $ply = $plys[$i]; |
532 | $comment = $ply[2][2]; |
533 | if ( $comment !== null ) { |
534 | $span['annotations'][] = [ 'comment' => $comment ]; |
535 | } |
536 | if ( ( $index + $i ) % 2 === 0 ) { |
537 | $moveNumber = ( ( $index + $i ) / 2 ) + 1; |
538 | $span['step-link'] = true; |
539 | $span['move-number'] = $moveNumber; |
540 | } |
541 | $span['variation-ply'] = $i; |
542 | $span['variation-token'] = $token; |
543 | $spanList[] = $span; |
544 | } |
545 | return $spanList; |
546 | } |
547 | |
548 | /** |
549 | * Create array of mustache arguments for ChessBoard.mustache from a given |
550 | * associative array of tag pairs |
551 | * @since 0.2.0 |
552 | * @param array $tagPairs |
553 | * @return array |
554 | */ |
555 | public static function getMetadata( $tagPairs ): array { |
556 | // TODO localize the defaults |
557 | $metadata = [ |
558 | 'event' => 'Unknown event', |
559 | 'site' => 'Unknown site', |
560 | 'date' => 'Unknown date', |
561 | 'round' => 'Unkown round', |
562 | 'white' => 'Unknown white', |
563 | 'black' => 'Unknown black', |
564 | 'result' => 'Unknown result', |
565 | 'other-metadata' => [] |
566 | ]; |
567 | |
568 | foreach ( $tagPairs as $key => $value ) { |
569 | if ( array_key_exists( $key, $metadata ) ) { |
570 | $metadata[$key] = $value; |
571 | continue; |
572 | } |
573 | $metadata['other-metadata'][] = [ |
574 | 'label' => $key, |
575 | 'value' => $value |
576 | ]; |
577 | } |
578 | return $metadata; |
579 | } |
580 | |
581 | /** |
582 | * Create an array of arguments for chess-piece.mustache for a single piece |
583 | * at a given location on the board |
584 | * @since 0.2.0 |
585 | * @param string $symbol The FEN symbol for the piece |
586 | * @param string|int $rank Preserves input type on output |
587 | * @param string|int $file Preserves input type on output |
588 | * @return array |
589 | */ |
590 | public static function createPiece( $symbol, $rank, $file ): array { |
591 | if ( $rank > 7 || $file > 7 || $rank < 0 || $file < 0 ) { |
592 | throw new ChessBrowserException( "Impossible rank ($rank) or file ($file)" ); |
593 | } |
594 | |
595 | $validTypes = [ 'b', 'k', 'n', 'p', 'q', 'r' ]; |
596 | $type = strtolower( $symbol ); |
597 | |
598 | if ( !in_array( $type, $validTypes ) ) { |
599 | throw new ChessBrowserException( "Invalid piece type $type" ); |
600 | } |
601 | |
602 | $color = ( $type === $symbol ? 'd' : 'l' ); |
603 | |
604 | return [ |
605 | 'piece-type' => $type, |
606 | 'piece-color' => $color, |
607 | 'piece-rank' => $rank, |
608 | 'piece-file' => $file |
609 | ]; |
610 | } |
611 | } |