@alepot55/chessboardjs 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.eslintrc.json +227 -0
  2. package/README.md +125 -401
  3. package/assets/themes/alepot/theme.json +42 -0
  4. package/assets/themes/default/theme.json +42 -0
  5. package/chessboard.bundle.js +708 -58
  6. package/config/jest.config.js +15 -0
  7. package/config/rollup.config.js +36 -0
  8. package/dist/chessboard.cjs.js +10690 -0
  9. package/dist/chessboard.css +228 -0
  10. package/dist/chessboard.esm.js +10621 -0
  11. package/dist/chessboard.iife.js +10696 -0
  12. package/dist/chessboard.umd.js +10696 -0
  13. package/jest.config.js +2 -7
  14. package/package.json +18 -3
  15. package/rollup.config.js +2 -11
  16. package/{chessboard.move.js → src/components/Move.js} +3 -3
  17. package/src/components/Piece.js +288 -0
  18. package/{chessboard.square.js → src/components/Square.js} +60 -7
  19. package/src/constants/index.js +15 -0
  20. package/src/constants/positions.js +62 -0
  21. package/src/core/Chessboard.js +1939 -0
  22. package/src/core/ChessboardConfig.js +458 -0
  23. package/src/core/ChessboardFactory.js +385 -0
  24. package/src/core/index.js +141 -0
  25. package/src/errors/ChessboardError.js +133 -0
  26. package/src/errors/index.js +15 -0
  27. package/src/errors/messages.js +189 -0
  28. package/src/index.js +103 -0
  29. package/src/services/AnimationService.js +180 -0
  30. package/src/services/BoardService.js +156 -0
  31. package/src/services/CoordinateService.js +355 -0
  32. package/src/services/EventService.js +955 -0
  33. package/src/services/MoveService.js +629 -0
  34. package/src/services/PieceService.js +312 -0
  35. package/src/services/PositionService.js +237 -0
  36. package/src/services/ValidationService.js +673 -0
  37. package/src/services/index.js +14 -0
  38. package/src/styles/animations.css +46 -0
  39. package/{chessboard.css → src/styles/board.css} +8 -4
  40. package/src/styles/index.css +4 -0
  41. package/src/styles/pieces.css +70 -0
  42. package/src/utils/animations.js +37 -0
  43. package/{chess.js → src/utils/chess.js} +16 -16
  44. package/src/utils/coordinates.js +62 -0
  45. package/src/utils/cross-browser.js +150 -0
  46. package/src/utils/logger.js +422 -0
  47. package/src/utils/performance.js +311 -0
  48. package/src/utils/validation.js +458 -0
  49. package/tests/unit/chessboard-config-animations.test.js +106 -0
  50. package/tests/unit/chessboard-robust.test.js +163 -0
  51. package/tests/unit/chessboard.test.js +183 -0
  52. package/chessboard.config.js +0 -147
  53. package/chessboard.js +0 -979
  54. package/chessboard.piece.js +0 -115
  55. package/test/chessboard.test.js +0 -128
  56. /package/{alepot_theme → assets/themes/alepot}/bb.svg +0 -0
  57. /package/{alepot_theme → assets/themes/alepot}/bw.svg +0 -0
  58. /package/{alepot_theme → assets/themes/alepot}/kb.svg +0 -0
  59. /package/{alepot_theme → assets/themes/alepot}/kw.svg +0 -0
  60. /package/{alepot_theme → assets/themes/alepot}/nb.svg +0 -0
  61. /package/{alepot_theme → assets/themes/alepot}/nw.svg +0 -0
  62. /package/{alepot_theme → assets/themes/alepot}/pb.svg +0 -0
  63. /package/{alepot_theme → assets/themes/alepot}/pw.svg +0 -0
  64. /package/{alepot_theme → assets/themes/alepot}/qb.svg +0 -0
  65. /package/{alepot_theme → assets/themes/alepot}/qw.svg +0 -0
  66. /package/{alepot_theme → assets/themes/alepot}/rb.svg +0 -0
  67. /package/{alepot_theme → assets/themes/alepot}/rw.svg +0 -0
  68. /package/{default_pieces → assets/themes/default}/bb.svg +0 -0
  69. /package/{default_pieces → assets/themes/default}/bw.svg +0 -0
  70. /package/{default_pieces → assets/themes/default}/kb.svg +0 -0
  71. /package/{default_pieces → assets/themes/default}/kw.svg +0 -0
  72. /package/{default_pieces → assets/themes/default}/nb.svg +0 -0
  73. /package/{default_pieces → assets/themes/default}/nw.svg +0 -0
  74. /package/{default_pieces → assets/themes/default}/pb.svg +0 -0
  75. /package/{default_pieces → assets/themes/default}/pw.svg +0 -0
  76. /package/{default_pieces → assets/themes/default}/qb.svg +0 -0
  77. /package/{default_pieces → assets/themes/default}/qw.svg +0 -0
  78. /package/{default_pieces → assets/themes/default}/rb.svg +0 -0
  79. /package/{default_pieces → assets/themes/default}/rw.svg +0 -0
  80. /package/{.babelrc → config/.babelrc} +0 -0
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Service for managing chess pieces and their operations
3
+ * @module services/PieceService
4
+ * @since 2.0.0
5
+ */
6
+
7
+ import Piece from '../components/Piece.js';
8
+ import { PieceError, ValidationError } from '../errors/ChessboardError.js';
9
+ import { ERROR_MESSAGES } from '../errors/messages.js';
10
+ import { PIECE_TYPES, PIECE_COLORS } from '../constants/positions.js';
11
+ import { DragOptimizations } from '../utils/cross-browser.js';
12
+
13
+ /**
14
+ * Service responsible for piece management and operations
15
+ * @class
16
+ */
17
+ export class PieceService {
18
+ /**
19
+ * Creates a new PieceService instance
20
+ * @param {ChessboardConfig} config - Board configuration
21
+ */
22
+ constructor(config) {
23
+ this.config = config;
24
+ }
25
+
26
+ /**
27
+ * Gets the path to a piece asset
28
+ * @param {string} piece - Piece identifier (e.g., 'wK', 'bP')
29
+ * @returns {string} Path to piece asset
30
+ * @throws {ValidationError} When piecesPath configuration is invalid
31
+ */
32
+ getPiecePath(piece) {
33
+ const { piecesPath } = this.config;
34
+
35
+ if (typeof piecesPath === 'string') {
36
+ return `${piecesPath}/${piece}.svg`;
37
+ } else if (typeof piecesPath === 'object' && piecesPath !== null) {
38
+ return piecesPath[piece];
39
+ } else if (typeof piecesPath === 'function') {
40
+ return piecesPath(piece);
41
+ } else {
42
+ throw new ValidationError(ERROR_MESSAGES.invalid_piecesPath, 'piecesPath', piecesPath);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Converts various piece formats to a Piece instance
48
+ * @param {string|Piece} piece - Piece in various formats
49
+ * @returns {Piece} Piece instance
50
+ * @throws {PieceError} When piece format is invalid
51
+ */
52
+ convertPiece(piece) {
53
+ if (piece instanceof Piece) {
54
+ return piece;
55
+ }
56
+
57
+ if (typeof piece === 'string' && piece.length === 2) {
58
+ const [first, second] = piece.split('');
59
+ let type, color;
60
+
61
+ // Check format: [type][color] (e.g., 'pw')
62
+ if (PIECE_TYPES.includes(first.toLowerCase()) && PIECE_COLORS.includes(second)) {
63
+ type = first.toLowerCase();
64
+ color = second;
65
+ }
66
+ // Check format: [color][type] (e.g., 'wP')
67
+ else if (PIECE_COLORS.includes(first) && PIECE_TYPES.includes(second.toLowerCase())) {
68
+ color = first;
69
+ type = second.toLowerCase();
70
+ } else {
71
+ throw new PieceError(ERROR_MESSAGES.invalid_piece + piece, piece);
72
+ }
73
+
74
+ const piecePath = this.getPiecePath(type + color);
75
+ return new Piece(color, type, piecePath);
76
+ }
77
+
78
+ throw new PieceError(ERROR_MESSAGES.invalid_piece + piece, piece);
79
+ }
80
+
81
+ /**
82
+ * Adds a piece to a square with optional fade-in animation
83
+ * @param {Square} square - Target square
84
+ * @param {Piece} piece - Piece to add
85
+ * @param {boolean} [fade=true] - Whether to fade in the piece
86
+ * @param {Function} dragFunction - Function to handle drag events
87
+ * @param {Function} [callback] - Callback when animation completes
88
+ */
89
+ addPieceOnSquare(square, piece, fade = true, dragFunction, callback) {
90
+ console.debug(`[PieceService] addPieceOnSquare: ${piece.id} to ${square.id}`);
91
+ square.putPiece(piece);
92
+
93
+ if (dragFunction) {
94
+ piece.setDrag(dragFunction(square, piece));
95
+ }
96
+
97
+ if (fade && this.config.fadeTime > 0) {
98
+ piece.fadeIn(
99
+ this.config.fadeTime,
100
+ this.config.fadeAnimation,
101
+ this._getTransitionTimingFunction(),
102
+ callback
103
+ );
104
+ } else {
105
+ if (callback) callback();
106
+ }
107
+
108
+ piece.visible();
109
+ }
110
+
111
+ /**
112
+ * Removes a piece from a square with optional fade-out animation
113
+ * @param {Square} square - Source square
114
+ * @param {boolean} [fade=true] - Whether to fade out the piece
115
+ * @param {Function} [callback] - Callback when animation completes
116
+ * @returns {Piece} The removed piece
117
+ * @throws {PieceError} When square has no piece to remove
118
+ */
119
+ removePieceFromSquare(square, fade = true, callback) {
120
+ console.debug(`[PieceService] removePieceFromSquare: ${square.id}`);
121
+ square.check();
122
+
123
+ const piece = square.piece;
124
+ if (!piece) {
125
+ if (callback) callback();
126
+ throw new PieceError(ERROR_MESSAGES.square_no_piece, null, square.getId());
127
+ }
128
+
129
+ if (fade && this.config.fadeTime > 0) {
130
+ piece.fadeOut(
131
+ this.config.fadeTime,
132
+ this.config.fadeAnimation,
133
+ this._getTransitionTimingFunction(),
134
+ callback
135
+ );
136
+ } else {
137
+ if (callback) callback();
138
+ }
139
+
140
+ square.removePiece();
141
+ return piece;
142
+ }
143
+
144
+ /**
145
+ * Moves a piece to a new position with animation
146
+ * @param {Piece} piece - Piece to move
147
+ * @param {Square} targetSquare - Target square
148
+ * @param {number} duration - Animation duration
149
+ * @param {Function} [callback] - Callback function when animation completes
150
+ */
151
+ movePiece(piece, targetSquare, duration, callback) {
152
+ console.debug(`[PieceService] movePiece: ${piece.id} to ${targetSquare.id}`);
153
+ if (!piece) {
154
+ console.warn('PieceService.movePiece: piece is null, skipping animation');
155
+ if (callback) callback();
156
+ return;
157
+ }
158
+
159
+ piece.translate(
160
+ targetSquare,
161
+ duration,
162
+ this._getTransitionTimingFunction(),
163
+ this.config.moveAnimation,
164
+ callback
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Handles piece translation with optional capture
170
+ * @param {Move} move - Move object containing from/to squares and piece
171
+ * @param {boolean} removeTarget - Whether to remove piece from target square
172
+ * @param {boolean} animate - Whether to animate the move
173
+ * @param {Function} [dragFunction] - Function to create drag handlers
174
+ * @param {Function} [callback] - Callback function when complete
175
+ */
176
+ translatePiece(move, removeTarget, animate, dragFunction = null, callback = null) {
177
+ console.debug(`[PieceService] translatePiece: ${move.piece.id} from ${move.from.id} to ${move.to.id}`);
178
+ if (!move.piece) {
179
+ console.warn('PieceService.translatePiece: move.piece is null, skipping translation');
180
+ if (callback) callback();
181
+ return;
182
+ }
183
+
184
+ if (removeTarget) {
185
+ // Deselect the captured piece before removing it
186
+ move.to.deselect();
187
+ this.removePieceFromSquare(move.to, false);
188
+ }
189
+
190
+ const changeSquareCallback = () => {
191
+ // Check if piece still exists and is on the source square
192
+ if (move.from.piece === move.piece) {
193
+ move.from.removePiece(true); // Preserve the piece when moving
194
+ }
195
+
196
+ // Only put piece if destination square doesn't already have it
197
+ if (move.to.piece !== move.piece) {
198
+ move.to.putPiece(move.piece);
199
+
200
+ // Re-attach drag handler if provided
201
+ if (dragFunction && this.config.draggable && move.piece.element) {
202
+ move.piece.setDrag(dragFunction(move.to, move.piece));
203
+ }
204
+ }
205
+
206
+ if (callback) callback();
207
+ };
208
+
209
+ // Check if piece is currently being dragged
210
+ const isDragging = move.piece.element.classList.contains('dragging');
211
+
212
+ if (isDragging) {
213
+ // If piece is being dragged, don't animate - just move it immediately
214
+ // The piece is already visually in the correct position from the drag
215
+ changeSquareCallback();
216
+ } else {
217
+ // Normal animation
218
+ const duration = animate ? this.config.moveTime : 0;
219
+ this.movePiece(move.piece, move.to, duration, changeSquareCallback);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Snaps a piece back to its original position
225
+ * @param {Square} square - Square containing the piece
226
+ * @param {boolean} [animate=true] - Whether to animate the snapback
227
+ */
228
+ snapbackPiece(square, animate = true) {
229
+ if (!square || !square.piece) {
230
+ return;
231
+ }
232
+ const piece = square.piece;
233
+ const duration = animate ? this.config.snapbackTime : 0;
234
+ console.debug(`[PieceService] snapbackPiece: ${piece.id} on ${square.id}`);
235
+ piece.translate(
236
+ square,
237
+ duration,
238
+ this._getTransitionTimingFunction(),
239
+ this.config.snapbackAnimation
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Centers a piece in its square with animation (after successful drop)
245
+ * @param {Square} square - Square containing the piece to center
246
+ * @param {boolean} animate - Whether to animate the centering
247
+ */
248
+ centerPiece(square, animate = true) {
249
+ if (!square || !square.piece) {
250
+ return;
251
+ }
252
+ const piece = square.piece;
253
+ const duration = animate ? this.config.dropCenterTime : 0;
254
+ console.debug(`[PieceService] centerPiece: ${piece.id} on ${square.id}`);
255
+ piece.translate(
256
+ square,
257
+ duration,
258
+ this._getTransitionTimingFunction(),
259
+ this.config.dropCenterAnimation,
260
+ () => {
261
+ // After animation, reset all drag-related styles
262
+ if (!piece.element) return;
263
+ piece.element.style.position = '';
264
+ piece.element.style.left = '';
265
+ piece.element.style.top = '';
266
+ piece.element.style.transform = '';
267
+ piece.element.style.zIndex = '';
268
+ // Remove inline dimensions set during drag
269
+ piece.element.style.width = '';
270
+ piece.element.style.height = '';
271
+ // Remove dragging class
272
+ piece.element.classList.remove('dragging');
273
+
274
+ // Clean up drag optimizations that might interfere with click
275
+ DragOptimizations.cleanupAfterDrag(piece.element);
276
+ }
277
+ );
278
+ }
279
+
280
+ /**
281
+ * Gets the transition timing function for animations
282
+ * @private
283
+ * @returns {Function} Timing function
284
+ */
285
+ _getTransitionTimingFunction() {
286
+ return (elapsed, duration, type = 'ease') => {
287
+ const x = elapsed / duration;
288
+
289
+ switch (type) {
290
+ case 'linear':
291
+ return x;
292
+ case 'ease':
293
+ return (x ** 2) * (3 - 2 * x);
294
+ case 'ease-in':
295
+ return x ** 2;
296
+ case 'ease-out':
297
+ return -1 * (x - 1) ** 2 + 1;
298
+ case 'ease-in-out':
299
+ return (x < 0.5) ? 2 * x ** 2 : 4 * x - 2 * x ** 2 - 1;
300
+ default:
301
+ return x;
302
+ }
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Cleans up resources
308
+ */
309
+ destroy() {
310
+ // Cleanup any cached pieces or references
311
+ }
312
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Service for managing chess positions and FEN conversion
3
+ * @module services/PositionService
4
+ * @since 2.0.0
5
+ */
6
+
7
+ import { Chess, validateFen } from '../utils/chess.js';
8
+ import { STANDARD_POSITIONS, DEFAULT_STARTING_POSITION, BOARD_LETTERS } from '../constants/positions.js';
9
+ import { ValidationError } from '../errors/ChessboardError.js';
10
+ import { ERROR_MESSAGES } from '../errors/messages.js';
11
+
12
+ /**
13
+ * Service responsible for position management and FEN operations
14
+ * @class
15
+ */
16
+ export class PositionService {
17
+ /**
18
+ * Creates a new PositionService instance
19
+ * @param {ChessboardConfig} config - Board configuration
20
+ */
21
+ constructor(config) {
22
+ this.config = config;
23
+ this.game = null;
24
+ }
25
+
26
+ /**
27
+ * Converts various position formats to FEN string
28
+ * @param {string|Object} position - Position in various formats
29
+ * @returns {string} FEN string representation
30
+ * @throws {ValidationError} When position format is invalid
31
+ */
32
+ convertFen(position) {
33
+ if (typeof position === 'string') {
34
+ return this._convertStringPosition(position);
35
+ } else if (typeof position === 'object' && position !== null) {
36
+ return this._convertObjectPosition(position);
37
+ } else {
38
+ throw new ValidationError(ERROR_MESSAGES.invalid_position + position, 'position', position);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Converts string position to FEN
44
+ * @private
45
+ * @param {string} position - String position
46
+ * @returns {string} FEN string
47
+ */
48
+ _convertStringPosition(position) {
49
+ if (position === 'start') {
50
+ return DEFAULT_STARTING_POSITION;
51
+ }
52
+
53
+ if (this.validateFen(position)) {
54
+ return position;
55
+ }
56
+
57
+ if (STANDARD_POSITIONS[position]) {
58
+ return STANDARD_POSITIONS[position];
59
+ }
60
+
61
+ throw new ValidationError(ERROR_MESSAGES.invalid_position + position, 'position', position);
62
+ }
63
+
64
+ /**
65
+ * Converts object position to FEN
66
+ * @private
67
+ * @param {Object} position - Object with square->piece mapping
68
+ * @returns {string} FEN string
69
+ */
70
+ _convertObjectPosition(position) {
71
+ const parts = [];
72
+
73
+ for (let row = 0; row < 8; row++) {
74
+ const rowParts = [];
75
+ let empty = 0;
76
+
77
+ for (let col = 0; col < 8; col++) {
78
+ const square = this._getSquareID(row, col);
79
+ const piece = position[square];
80
+
81
+ if (piece) {
82
+ if (empty > 0) {
83
+ rowParts.push(empty);
84
+ empty = 0;
85
+ }
86
+ // Convert piece notation: white pieces become uppercase, black remain lowercase
87
+ const fenPiece = piece[1] === 'w' ? piece[0].toUpperCase() : piece[0].toLowerCase();
88
+ rowParts.push(fenPiece);
89
+ } else {
90
+ empty++;
91
+ }
92
+ }
93
+
94
+ if (empty > 0) {
95
+ rowParts.push(empty);
96
+ }
97
+
98
+ parts.push(rowParts.join(''));
99
+ }
100
+
101
+ return parts.join('/') + ' w KQkq - 0 1';
102
+ }
103
+
104
+ /**
105
+ * Sets up the game with the given position
106
+ * @param {string|Object} position - Position to set
107
+ * @param {Object} [options] - Additional options for game setup
108
+ */
109
+ setGame(position, options = {}) {
110
+ const fen = this.convertFen(position);
111
+
112
+ if (this.game) {
113
+ this.game.load(fen, options);
114
+ } else {
115
+ this.game = new Chess(fen);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Gets the current game instance
121
+ * @returns {Chess} Current chess.js game instance
122
+ */
123
+ getGame() {
124
+ return this.game;
125
+ }
126
+
127
+ /**
128
+ * Validates a FEN string
129
+ * @param {string} fen - FEN string to validate
130
+ * @returns {boolean} True if valid, false otherwise
131
+ */
132
+ validateFen(fen) {
133
+ return validateFen(fen);
134
+ }
135
+
136
+ /**
137
+ * Gets piece information for a specific square
138
+ * @param {string} squareId - Square identifier
139
+ * @returns {string|null} Piece ID or null if no piece
140
+ */
141
+ getGamePieceId(squareId) {
142
+ if (!this.game) return null;
143
+ const piece = this.game.get(squareId);
144
+ return piece ? piece.color + piece.type : null;
145
+ }
146
+
147
+ /**
148
+ * Checks if a specific piece is on a specific square
149
+ * @param {string} piece - Piece ID to check
150
+ * @param {string} square - Square to check
151
+ * @returns {boolean} True if piece is on square
152
+ */
153
+ isPiece(piece, square) {
154
+ return this.getGamePieceId(square) === piece;
155
+ }
156
+
157
+ /**
158
+ * Converts board coordinates to square ID
159
+ * @private
160
+ * @param {number} row - Row index (0-7)
161
+ * @param {number} col - Column index (0-7)
162
+ * @returns {string} Square ID (e.g., 'e4')
163
+ */
164
+ _getSquareID(row, col) {
165
+ row = parseInt(row);
166
+ col = parseInt(col);
167
+
168
+ if (this.config.orientation === 'w') {
169
+ row = 8 - row;
170
+ col = col + 1;
171
+ } else {
172
+ row = row + 1;
173
+ col = 8 - col;
174
+ }
175
+
176
+ const letter = BOARD_LETTERS[col - 1];
177
+ return letter + row;
178
+ }
179
+
180
+ /**
181
+ * Changes the turn in a FEN string
182
+ * @param {string} fen - Original FEN string
183
+ * @param {string} color - New turn color ('w' or 'b')
184
+ * @returns {string} Modified FEN string
185
+ */
186
+ changeFenTurn(fen, color) {
187
+ const parts = fen.split(' ');
188
+ parts[1] = color;
189
+ return parts.join(' ');
190
+ }
191
+
192
+ /**
193
+ * Gets the current position as an object
194
+ * @returns {Object} Position object with piece placements
195
+ */
196
+ getPosition() {
197
+ const position = {};
198
+ const game = this.getGame();
199
+
200
+ // Convert chess.js board to position object
201
+ const squares = ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1',
202
+ 'a2', 'b2', 'c2', 'd2', 'e2', 'f2', 'g2', 'h2',
203
+ 'a3', 'b3', 'c3', 'd3', 'e3', 'f3', 'g3', 'h3',
204
+ 'a4', 'b4', 'c4', 'd4', 'e4', 'f4', 'g4', 'h4',
205
+ 'a5', 'b5', 'c5', 'd5', 'e5', 'f5', 'g5', 'h5',
206
+ 'a6', 'b6', 'c6', 'd6', 'e6', 'f6', 'g6', 'h6',
207
+ 'a7', 'b7', 'c7', 'd7', 'e7', 'f7', 'g7', 'h7',
208
+ 'a8', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8'];
209
+
210
+ for (const square of squares) {
211
+ const piece = game.get(square);
212
+ if (piece) {
213
+ position[square] = piece.type + piece.color;
214
+ }
215
+ }
216
+
217
+ return position;
218
+ }
219
+
220
+ /**
221
+ * Toggles the turn in a FEN string
222
+ * @param {string} fen - Original FEN string
223
+ * @returns {string} Modified FEN string
224
+ */
225
+ changeFenColor(fen) {
226
+ const parts = fen.split(' ');
227
+ parts[1] = parts[1] === 'w' ? 'b' : 'w';
228
+ return parts.join(' ');
229
+ }
230
+
231
+ /**
232
+ * Cleans up resources
233
+ */
234
+ destroy() {
235
+ this.game = null;
236
+ }
237
+ }