kokopu-react 1.3.1 → 1.5.0

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 (123) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/lib/ArrowMarkerIcon.js +5 -7
  4. package/dist/lib/Chessboard.js +23 -33
  5. package/dist/lib/ErrorBox.js +85 -5
  6. package/dist/lib/Movetext.js +839 -0
  7. package/dist/lib/SquareMarkerIcon.js +1 -1
  8. package/dist/lib/TextMarkerIcon.js +12 -11
  9. package/dist/lib/css/arrow.css +1 -1
  10. package/dist/lib/css/chessboard.css +6 -1
  11. package/dist/lib/css/error_box.css +13 -2
  12. package/dist/lib/css/fonts.css +34 -0
  13. package/dist/lib/css/movetext.css +117 -0
  14. package/dist/lib/css/{label.css → symbol.css} +6 -2
  15. package/dist/lib/fonts/chessalpha.woff +0 -0
  16. package/dist/lib/fonts/chessalpha.woff2 +0 -0
  17. package/dist/lib/i18n.js +17 -2
  18. package/dist/lib/impl/ArrowTip.js +3 -8
  19. package/dist/lib/impl/HtmlSanitizer.js +171 -0
  20. package/dist/lib/impl/TextSymbol.js +98 -0
  21. package/dist/lib/impl/colorsets.js +1 -1
  22. package/dist/lib/impl/piecesets.js +1 -1
  23. package/dist/lib/impl/util.js +6 -6
  24. package/dist/lib/index.js +32 -24
  25. package/dist/lib/markers.js +10 -10
  26. package/doc_src/demo/PageChessboardBase.js +1 -1
  27. package/doc_src/demo/{PageChessboardEdition.js → PageChessboardInteraction.js} +4 -5
  28. package/doc_src/demo/PageChessboardMove.js +1 -1
  29. package/doc_src/demo/PageChessboardSmallScreens.js +1 -1
  30. package/doc_src/demo/PageMovetextBase.js +214 -0
  31. package/doc_src/demo/PageMovetextInteraction.js +155 -0
  32. package/doc_src/demo/demo.css +8 -1
  33. package/doc_src/demo/game-1.pgn +23 -0
  34. package/doc_src/demo/game-2.pgn +22 -0
  35. package/doc_src/demo/game-invalid.pgn +10 -0
  36. package/doc_src/demo/util.js +1 -1
  37. package/doc_src/examples/Movetext.md +15 -0
  38. package/doc_src/home.md +2 -0
  39. package/doc_src/theming/LogoRenderer.js +1 -1
  40. package/doc_src/theming/kokopu-react-favicon.png +0 -0
  41. package/doc_src/theming/theming.css +1 -1
  42. package/graphic_test_app/01_marker_icons.js +6 -4
  43. package/graphic_test_app/02_chessboard_simple.js +1 -1
  44. package/graphic_test_app/03_chessboard_flipped.js +1 -1
  45. package/graphic_test_app/04_chessboard_annotations.js +2 -2
  46. package/graphic_test_app/05_chessboard_move.js +1 -1
  47. package/graphic_test_app/06_chessboard_theme.js +1 -1
  48. package/graphic_test_app/07_chessboard_click_squares.js +1 -1
  49. package/graphic_test_app/08_chessboard_move_pieces.js +1 -1
  50. package/graphic_test_app/09_chessboard_edit_arrows.js +1 -1
  51. package/graphic_test_app/10_chessboard_play_moves.js +1 -1
  52. package/graphic_test_app/11_chessboard_play_promotions.js +1 -1
  53. package/graphic_test_app/12_movetext_simple.js +41 -0
  54. package/graphic_test_app/13_movetext_error.js +38 -0
  55. package/graphic_test_app/14_movetext_html.js +40 -0
  56. package/graphic_test_app/15_movetext_options.js +42 -0
  57. package/graphic_test_app/16_movetext_interaction.js +42 -0
  58. package/graphic_test_app/common/dummy.pgn +8 -0
  59. package/graphic_test_app/common/games.pgn +167 -0
  60. package/graphic_test_app/common/heartbeat.txt +1 -0
  61. package/graphic_test_app/common/test_app.css +20 -4
  62. package/graphic_test_app/common/test_app.js +6 -6
  63. package/package.json +26 -25
  64. package/scripts/test_graphic/clean-graphic-output.js +1 -1
  65. package/scripts/test_graphic/docker-compose.yml +1 -1
  66. package/scripts/test_graphic/webpack-config.js +8 -3
  67. package/scripts/test_headless.webpack-config.js +3 -2
  68. package/src/ArrowMarkerIcon.js +2 -2
  69. package/src/Chessboard.js +11 -11
  70. package/src/ErrorBox.js +80 -1
  71. package/src/Movetext.js +616 -0
  72. package/src/SquareMarkerIcon.js +1 -1
  73. package/src/TextMarkerIcon.js +7 -7
  74. package/src/css/arrow.css +1 -1
  75. package/src/css/chessboard.css +6 -1
  76. package/src/css/error_box.css +13 -2
  77. package/src/css/fonts.css +34 -0
  78. package/src/css/movetext.css +117 -0
  79. package/src/css/{label.css → symbol.css} +6 -2
  80. package/src/fonts/chessalpha.woff +0 -0
  81. package/src/fonts/chessalpha.woff2 +0 -0
  82. package/src/i18n.js +12 -1
  83. package/src/impl/ArrowTip.js +3 -7
  84. package/src/impl/HtmlSanitizer.js +120 -0
  85. package/src/impl/TextSymbol.js +76 -0
  86. package/src/impl/colorsets.js +1 -1
  87. package/src/impl/piecesets.js +1 -1
  88. package/src/impl/util.js +2 -2
  89. package/src/index.js +2 -1
  90. package/src/markers.js +8 -7
  91. package/test/1_markers.js +20 -1
  92. package/test/2_chessboard_util.js +1 -1
  93. package/test/3_marker_icons_graphic.js +2 -2
  94. package/test/4_chessboard_graphic.js +1 -1
  95. package/test/5_chessboard_interaction.js +1 -1
  96. package/test/6_chessboard_play_moves.js +1 -1
  97. package/test/7_movetext_graphic.js +43 -0
  98. package/test/8_movetext_interaction.js +99 -0
  99. package/test/common/graphic.js +2 -2
  100. package/test/references/01_marker_icons/10.png +0 -0
  101. package/test/references/01_marker_icons/9.png +0 -0
  102. package/test/references/04_chessboard_annotations/4.png +0 -0
  103. package/test/references/04_chessboard_annotations/5.png +0 -0
  104. package/test/references/05_chessboard_move/3.png +0 -0
  105. package/test/references/12_movetext_simple/0.png +0 -0
  106. package/test/references/12_movetext_simple/1.png +0 -0
  107. package/test/references/12_movetext_simple/2.png +0 -0
  108. package/test/references/12_movetext_simple/3.png +0 -0
  109. package/test/references/13_movetext_error/0.png +0 -0
  110. package/test/references/13_movetext_error/1.png +0 -0
  111. package/test/references/13_movetext_error/2.png +0 -0
  112. package/test/references/13_movetext_error/3.png +0 -0
  113. package/test/references/14_movetext_html/0.png +0 -0
  114. package/test/references/14_movetext_html/1.png +0 -0
  115. package/test/references/14_movetext_html/2.png +0 -0
  116. package/test/references/15_movetext_options/0.png +0 -0
  117. package/test/references/15_movetext_options/1.png +0 -0
  118. package/test/references/15_movetext_options/2.png +0 -0
  119. package/test/references/15_movetext_options/3.png +0 -0
  120. package/test/references/16_movetext_interaction/0.png +0 -0
  121. package/test/references/16_movetext_interaction/1.png +0 -0
  122. package/test/references/16_movetext_interaction/2.png +0 -0
  123. package/graphic_test_app/common/healthcheck.txt +0 -1
@@ -0,0 +1,616 @@
1
+ /******************************************************************************
2
+ * *
3
+ * This file is part of Kokopu-React, a JavaScript chess library. *
4
+ * Copyright (C) 2021-2022 Yoann Le Montagner <yo35 -at- melix.net> *
5
+ * *
6
+ * This program is free software: you can redistribute it and/or *
7
+ * modify it under the terms of the GNU Lesser General Public License *
8
+ * as published by the Free Software Foundation, either version 3 of *
9
+ * the License, or (at your option) any later version. *
10
+ * *
11
+ * This program is distributed in the hope that it will be useful, *
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14
+ * GNU Lesser General Public License for more details. *
15
+ * *
16
+ * You should have received a copy of the GNU Lesser General *
17
+ * Public License along with this program. If not, see *
18
+ * <http://www.gnu.org/licenses/>. *
19
+ * *
20
+ ******************************************************************************/
21
+
22
+
23
+ import PropTypes from 'prop-types';
24
+ import React from 'react';
25
+ import kokopu from 'kokopu';
26
+
27
+ import HtmlSanitizer from './impl/HtmlSanitizer';
28
+ import Chessboard from './Chessboard';
29
+ import ErrorBox from './ErrorBox';
30
+ import i18n from './i18n';
31
+
32
+ import './css/fonts.css';
33
+ import './css/movetext.css';
34
+
35
+
36
+ /**
37
+ * Display a chess game, i.e. the headers (name of the players, event, etc.), the moves, and all the related annotations if any (comments, variations, NAGs...).
38
+ */
39
+ export default class Movetext extends React.Component {
40
+
41
+ constructor(props) {
42
+ super(props);
43
+ this.focusFieldRef = React.createRef();
44
+ }
45
+
46
+ render() {
47
+ let info = parseGame(this.props.game, this.props.gameIndex);
48
+ if (info.error) {
49
+ return <ErrorBox title={i18n.INVALID_PGN_ERROR_TITLE} message={info.message} text={info.text} errorIndex={info.errorIndex} lineNumber={info.lineNumber}></ErrorBox>;
50
+ }
51
+ let game = info.game;
52
+
53
+ return (
54
+ <div className="kokopu-movetext">
55
+ {this.renderHeaders(game)}
56
+ {this.renderBody(game)}
57
+ {this.renderFocusField()}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ renderHeaders(game) {
63
+ if (!this.props.headerVisible) {
64
+ return undefined;
65
+ }
66
+ let headers = [];
67
+ headers.push(this.renderPlayerHeaders(game, 'w'));
68
+ headers.push(this.renderPlayerHeaders(game, 'b'));
69
+ headers.push(this.renderEventRoundHeaders(game));
70
+ headers.push(this.renderDateSiteHeaders(game));
71
+ headers.push(this.renderAnnotatorHeader(game));
72
+ return headers.some(element => element !== undefined) ? <div className="kokopu-headers">{headers}</div> : undefined;
73
+ }
74
+
75
+ /**
76
+ * Header containing the player-related information (name, rating, title) corresponding to the given color.
77
+ */
78
+ renderPlayerHeaders(game, color) {
79
+ let playerName = game.playerName(color);
80
+ if (playerName === undefined) {
81
+ return undefined;
82
+ }
83
+ let title = game.playerTitle(color);
84
+ let rating = game.playerElo(color);
85
+
86
+ let classNames = [ 'kokopu-headerGroup-player', color === 'w' ? 'kokopu-headerGroup-whitePlayer' : 'kokopu-headerGroup-blackPlayer' ];
87
+ let colorTag = <span className="kokopu-colorTag"></span>;
88
+ let playerNameElement = <span className="kokopu-header-playerName">{sanitizeHtml(playerName)}</span>;
89
+ let titleElement = title === undefined ? undefined : <span className="kokopu-header-playerTitle">{sanitizeHtml(title)}</span>;
90
+ let ratingElement = rating === undefined ? undefined : <span className="kokopu-header-playerRating">{sanitizeHtml(rating)}</span>;
91
+ let separator = title === undefined || rating === undefined ? undefined : '\u00a0'; // \u00a0 == &nbsp;
92
+ let titleRatingGroup = title === undefined && rating === undefined ? undefined : <span className="kokopu-headerGroup-titleRating">{titleElement}{separator}{ratingElement}</span>;
93
+ return <div className={classNames.join(' ')} key={'player-' + color}>{colorTag}{playerNameElement}{titleRatingGroup}</div>;
94
+ }
95
+
96
+ /**
97
+ * Header containing the event-related information: event + round.
98
+ */
99
+ renderEventRoundHeaders(game) {
100
+ let evt = game.event();
101
+ if (evt === undefined) {
102
+ return undefined;
103
+ }
104
+ let round = game.round();
105
+ let roundElement = round === undefined ? undefined : <span className="kokopu-header-round">{sanitizeHtml(round)}</span>;
106
+ let evtElement = <span className="kokopu-header-event">{sanitizeHtml(evt)}</span>;
107
+ return <div className="kokopu-headerGroup-eventRound" key="event-round">{evtElement}{roundElement}</div>;
108
+ }
109
+
110
+ /**
111
+ * Header containing the date/place information.
112
+ */
113
+ renderDateSiteHeaders(game) {
114
+ let date = game.date();
115
+ let site = game.site();
116
+ if(date === undefined && site === undefined) {
117
+ return undefined;
118
+ }
119
+ let dateElement = date === undefined ? undefined : <span className="kokopu-header-date">{capitalizeFirstWord(game.dateAsString())}</span>;
120
+ let siteElement = site === undefined ? undefined : <span className="kokopu-header-site">{sanitizeHtml(site)}</span>;
121
+ let separator = date === undefined || site === undefined ? undefined : '\u00a0\u2013\u00a0'; // \u00a0 == &nbsp; \u2013 == &ndash;
122
+ return <div className="kokopu-headerGroup-dateSite" key="date-site">{dateElement}{separator}{siteElement}</div>;
123
+ }
124
+
125
+ /**
126
+ * Header containing the annotator information.
127
+ */
128
+ renderAnnotatorHeader(game) {
129
+ let annotator = game.annotator();
130
+ if (annotator === undefined) {
131
+ return undefined;
132
+ }
133
+ annotator = i18n.ANNOTATED_BY.replace(/\{0\}/g, annotator);
134
+ return <div className="kokopu-header-annotator" key="annotator">{sanitizeHtml(annotator)}</div>;
135
+ }
136
+
137
+ renderFocusField() {
138
+ if (this.props.interactionMode !== 'selectMove') {
139
+ return undefined;
140
+ }
141
+ return (
142
+ <div className="kokopu-focusFieldContainer">
143
+ <a className="kokopu-focusField" href="#" ref={this.focusFieldRef} onKeyDown={evt => this.handleKeyDownInFocusField(evt)}></a>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ renderBody(game) {
149
+ return this.renderVariation(this.getNotationTextBuilder(), game.mainVariation(), 'main-variation', true, game.result());
150
+ }
151
+
152
+ /**
153
+ * Render the given variation and its sub-variations, recursively.
154
+ */
155
+ renderVariation(notationTextBuilder, variation, variationKey, isMainVariation, gameResult) {
156
+
157
+ let moveGroups = []; // ... and also long comments and long sub-variations
158
+ let currentMoveGroupElements = [];
159
+
160
+ // Close the current move group, if any.
161
+ function closeMoveGroup() {
162
+ if (!variation.isLongVariation() || currentMoveGroupElements.length === 0) {
163
+ return;
164
+ }
165
+ moveGroups.push(<div className="kokopu-moveGroup" key={'group-' + moveGroups.length}>{currentMoveGroupElements}</div>);
166
+ currentMoveGroupElements = [];
167
+ }
168
+
169
+ // Write the initial comment, if any.
170
+ let variationComment = this.extractComment(variation);
171
+ if (variationComment !== undefined) {
172
+ if (variation.isLongComment()) {
173
+ moveGroups.push(this.renderComment(variation, variationComment, true));
174
+ }
175
+ else {
176
+ currentMoveGroupElements.push(this.renderComment(variation, variationComment, true));
177
+ }
178
+ }
179
+
180
+ // Visit all the PGN nodes (one node per move) within the variation.
181
+ let forcePrintMoveNumber = true;
182
+ let node = variation.first();
183
+ while (node !== undefined) {
184
+
185
+ // Write the move, including directly related information (i.e. move number + NAGs).
186
+ currentMoveGroupElements.push(this.renderMove(notationTextBuilder, node, forcePrintMoveNumber));
187
+
188
+ // Write the comment (if any).
189
+ let nodeComment = this.extractComment(node);
190
+ if (nodeComment !== undefined) {
191
+ if (node.isLongComment()) {
192
+ closeMoveGroup();
193
+ moveGroups.push(this.renderComment(node, nodeComment, false));
194
+ }
195
+ else {
196
+ currentMoveGroupElements.push(this.renderComment(node, nodeComment, false));
197
+ }
198
+ }
199
+
200
+ // Write the sub-variations.
201
+ let hasNonEmptySubVariations = false;
202
+ for (let [index, subVariation] of node.variations().entries()) {
203
+ let subVariationElement = this.renderVariation(notationTextBuilder, subVariation, node.fullMoveNumber() + node.moveColor() + '-variation-' + index, false);
204
+ if (subVariationElement) {
205
+ if (subVariation.isLongVariation()) {
206
+ closeMoveGroup();
207
+ moveGroups.push(subVariationElement);
208
+ }
209
+ else {
210
+ currentMoveGroupElements.push(subVariationElement);
211
+ }
212
+ hasNonEmptySubVariations = true;
213
+ }
214
+ }
215
+
216
+ // Back to the current variation, go to the next move.
217
+ forcePrintMoveNumber = (nodeComment !== undefined || hasNonEmptySubVariations);
218
+ node = node.next();
219
+ }
220
+
221
+ // Append the game result at the end of the main variation.
222
+ if (isMainVariation && gameResult !== '*') {
223
+ currentMoveGroupElements.push(<span className="kokopu-gameResult" key="gameResult">{formatResult(gameResult)}</span>);
224
+ }
225
+
226
+ // Close the last move group, and return the result.
227
+ closeMoveGroup();
228
+ let elements = variation.isLongVariation() ? moveGroups : currentMoveGroupElements;
229
+ if (elements.length === 0) {
230
+ return undefined;
231
+ }
232
+ if (isMainVariation) {
233
+ return <div className="kokopu-variation kokopu-mainVariation" key={variationKey}>{elements}</div>;
234
+ }
235
+ else if (variation.isLongVariation()) {
236
+ return <div className="kokopu-variation kokopu-subVariation" key={variationKey}>{elements}</div>;
237
+ }
238
+ else {
239
+ return <span className="kokopu-variation kokopu-subVariation" key={variationKey}>{elements}</span>;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Render the given move, move number, and NAG (if any).
245
+ */
246
+ renderMove(notationTextBuilder, node, forcePrintMoveNumber) {
247
+
248
+ // Move number
249
+ let moveNumber = undefined;
250
+ if (forcePrintMoveNumber || node.moveColor() === 'w') {
251
+ let moveNumberText = node.fullMoveNumber() + (node.moveColor() === 'w' ? '.' : '\u2026');
252
+ moveNumber = <span className="kokopu-moveNumber">{moveNumberText}</span>;
253
+ }
254
+
255
+ // SAN notation.
256
+ let notationText = notationTextBuilder(node.notation());
257
+
258
+ // NAGs
259
+ let nagElements = node.nags().map(nag => <span className="kokopu-nag" key={nag}>{kokopu.nagSymbol(nag)}</span>);
260
+
261
+ // Class
262
+ let nodeId = node.id();
263
+ let moveClassNames = [ 'kokopu-move' ];
264
+ let onClick = undefined;
265
+ if (this.props.selection && this.props.selection === nodeId) {
266
+ moveClassNames.push('kokopu-selectedMove');
267
+ }
268
+ if (this.props.interactionMode === 'selectMove') {
269
+ moveClassNames.push('kokopu-clickableMove');
270
+ onClick = () => this.handleNodeClicked(nodeId);
271
+ }
272
+
273
+ return (
274
+ <span className={moveClassNames.join(' ')} key={node.fullMoveNumber() + node.moveColor()}>
275
+ <span className="kokopu-moveContent" onClick={onClick}>
276
+ {moveNumber}
277
+ <span className="kokopu-moveNotation">{notationText}</span>
278
+ {nagElements.length === 0 ? undefined : nagElements}
279
+ </span>
280
+ </span>
281
+ );
282
+ }
283
+
284
+ /**
285
+ * Render the given text comment with its diagrams, if any.
286
+ */
287
+ renderComment(node, comment, isVariation) {
288
+ let content;
289
+ if (this.props.diagramVisible && comment.includes('[#]')) {
290
+ content = [];
291
+ let sanitizer = createSanitizer();
292
+ let isFirstPart = true;
293
+ for (let [index, part] of comment.split('[#]').entries()) {
294
+ if (!isFirstPart) {
295
+ let position = isVariation ? node.initialPosition() : node.position();
296
+ let diagram = <Chessboard position={position} squareMarkers={node.tag('csl')} arrowMarkers={node.tag('cal')} textMarkers={node.tag('ctl')}
297
+ flipped={this.props.diagramOptions.flipped}
298
+ squareSize={this.props.diagramOptions.squareSize}
299
+ coordinateVisible={this.props.diagramOptions.coordinateVisible}
300
+ smallScreenLimits={this.props.diagramOptions.smallScreenLimits}
301
+ colorset={this.props.diagramOptions.colorset}
302
+ pieceset={this.props.diagramOptions.pieceset}
303
+ />;
304
+ content.push(<div className="kokopu-diagram" key={'diagram-' + index}>{diagram}</div>);
305
+ }
306
+ isFirstPart = false;
307
+ part = part.trim();
308
+ if (part.length !== 0) {
309
+ content.push(sanitizeHtml(part, sanitizer));
310
+ }
311
+ }
312
+ }
313
+ else {
314
+ content = sanitizeHtml(comment);
315
+ }
316
+ let key = isVariation ? 'initial-comment' : node.fullMoveNumber() + node.moveColor() + '-comment';
317
+ return node.isLongComment() ? <div className="kokopu-comment" key={key}>{content}</div> : <span className="kokopu-comment" key={key}>{content}</span>;
318
+ }
319
+
320
+ extractComment(node) {
321
+ let comment = node.comment();
322
+ if (comment) {
323
+
324
+ // Remove the diagrams if necessary.
325
+ if (!this.props.diagramVisible) {
326
+ comment = comment.replace(/\[#\]/g, ' ');
327
+ }
328
+
329
+ // Trim and sanitize the space characters.
330
+ comment = comment.replace(/\s+/g, ' ').trim();
331
+ }
332
+ return comment ? comment : undefined;
333
+ }
334
+
335
+ handleKeyDownInFocusField(evt) {
336
+ if (!this.props.selection) {
337
+ return;
338
+ }
339
+ let game = parseGame(this.props.game, this.props.gameIndex).game;
340
+ let nodeId = false;
341
+ if (this.props.selection === 'start') {
342
+ if (evt.key === 'ArrowRight') {
343
+ nodeId = getNextNodeId(game.mainVariation(), true);
344
+ }
345
+ else if (evt.key === 'End') {
346
+ nodeId = getLastNodeId(game.mainVariation(), true);
347
+ }
348
+ }
349
+ else {
350
+ let currentNode = game.findById(this.props.selection);
351
+ if (!currentNode) {
352
+ return;
353
+ }
354
+ if (evt.key === 'Home') {
355
+ nodeId = 'start';
356
+ }
357
+ else if (evt.key === 'ArrowLeft') {
358
+ nodeId = getPreviousNodeId(currentNode);
359
+ }
360
+ else if (evt.key === 'ArrowRight') {
361
+ nodeId = getNextNodeId(currentNode, false);
362
+ }
363
+ else if (evt.key === 'End') {
364
+ nodeId = getLastNodeId(currentNode, false);
365
+ }
366
+ }
367
+ if (nodeId && this.props.onMoveSelected) {
368
+ this.props.onMoveSelected(nodeId);
369
+ }
370
+ }
371
+
372
+ handleNodeClicked(nodeId) {
373
+ this.focusFieldRef.current.focus();
374
+ if (this.props.onMoveSelected) {
375
+ this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId);
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Return the square at the given location.
381
+ */
382
+ getNotationTextBuilder() {
383
+ let pieceSymbols = this.props.pieceSymbols;
384
+ if (pieceSymbols === 'localized') {
385
+ let mapping = i18n.PIECE_SYMBOLS;
386
+ return notation => notation.replace(/[KQRBNP]/g, match => mapping[match]);
387
+ }
388
+ else if (pieceSymbols === 'figurines') {
389
+ return notation => figurineNotation(notation, 'alpha');
390
+ }
391
+ else if (pieceSymbols !== 'native' && pieceSymbols && ['K', 'Q', 'R', 'B', 'N', 'P'].every(p => typeof pieceSymbols[p] === 'string')) {
392
+ return notation => notation.replace(/[KQRBNP]/g, match => pieceSymbols[match]);
393
+ }
394
+ else {
395
+ return notation => notation;
396
+ }
397
+ }
398
+ }
399
+
400
+
401
+ Movetext.propTypes = {
402
+
403
+ /**
404
+ * Displayed position. Can be a [kokopu.Game](https://kokopu.yo35.org/docs/Game.html) object,
405
+ * a [kokopu.Database](https://kokopu.yo35.org/docs/Database.html) object,
406
+ * or a [PGN string](https://en.wikipedia.org/wiki/Portable_Game_Notation).
407
+ */
408
+ game: PropTypes.oneOfType([
409
+ PropTypes.instanceOf(kokopu.Game),
410
+ PropTypes.instanceOf(kokopu.Database),
411
+ PropTypes.string
412
+ ]),
413
+
414
+ /**
415
+ * Index of the game to display (only if attribute `game` is a [kokopu.Database](https://kokopu.yo35.org/docs/Database.html)
416
+ * or a [PGN string](https://en.wikipedia.org/wiki/Portable_Game_Notation)): `0` for the first game of the database/PGN, `1` for the second one, etc.
417
+ * If omitted, the first game of the database/PGN is displayed.
418
+ */
419
+ gameIndex: PropTypes.number,
420
+
421
+ /**
422
+ * Options applicable to the diagrams in the comments. See [Chessboard](#/Components/Chessboard) for more details about each option.
423
+ */
424
+ diagramOptions: PropTypes.shape({
425
+ flipped: Chessboard.propTypes.flipped,
426
+ squareSize: Chessboard.propTypes.squareSize,
427
+ coordinateVisible: Chessboard.propTypes.coordinateVisible,
428
+ smallScreenLimits: Chessboard.propTypes.smallScreenLimits,
429
+ colorset: Chessboard.propTypes.colorset,
430
+ pieceset: Chessboard.propTypes.pieceset,
431
+ }),
432
+
433
+ /**
434
+ * Symbols to use for the chess pieces. Can be:
435
+ * - `'native'`: use the first letter of the piece names (in English),
436
+ * - `'localized'`: use the symbols defined by `i18n.PIECE_SYMBOLS`,
437
+ * - `'figurines'`: use the figurines,
438
+ * - or an object defining 6 string-valued properties named `K`, `Q`, `R`, `B`, `N` and `P`.
439
+ */
440
+ pieceSymbols: PropTypes.oneOfType([
441
+ PropTypes.oneOf([ 'native', 'localized', 'figurines' ]),
442
+ PropTypes.shape({
443
+ K: PropTypes.string.isRequired,
444
+ Q: PropTypes.string.isRequired,
445
+ R: PropTypes.string.isRequired,
446
+ B: PropTypes.string.isRequired,
447
+ N: PropTypes.string.isRequired,
448
+ P: PropTypes.string.isRequired
449
+ })
450
+ ]),
451
+
452
+ /**
453
+ * Whether the diagrams within the comments (if any) are displayed or not.
454
+ */
455
+ diagramVisible: PropTypes.bool,
456
+
457
+ /**
458
+ * Whether the game headers (if any) are displayed or not.
459
+ */
460
+ headerVisible: PropTypes.bool,
461
+
462
+ /**
463
+ * ID of the selected move (or `'start'` for the beginning of the main variation).
464
+ * Use [kokopu.Node#id](https://kokopu.yo35.org/docs/Node.html#id) to get the ID of a game move.
465
+ */
466
+ selection: PropTypes.string,
467
+
468
+ /**
469
+ * Type of action allowed with the mouse/keys on the component. If undefined, then the user cannot interact with the component.
470
+ *
471
+ * - `'selectMove'` allows the user to select a move.
472
+ */
473
+ interactionMode: PropTypes.oneOf([ 'selectMove' ]),
474
+
475
+ /**
476
+ * Callback invoked when the user clicks on a move (only if `interactionMode` is set to `'selectMove'`).
477
+ *
478
+ * @param {string?} nodeId ID of the selected move (as returned by [kokopu.Node#id](https://kokopu.yo35.org/docs/Node.html#id)),
479
+ * `'start'` for the beginning of the main variation, or `undefined` if the user unselects the previously selected move.
480
+ */
481
+ onMoveSelected: PropTypes.func,
482
+ };
483
+
484
+
485
+ Movetext.defaultProps = {
486
+ game: new kokopu.Game(),
487
+ gameIndex: 0,
488
+ diagramOptions: {},
489
+ pieceSymbols: 'native',
490
+ diagramVisible: true,
491
+ headerVisible: true,
492
+ };
493
+
494
+
495
+ /**
496
+ * Example: `'hello world'` is turned into `'Hello world'`.
497
+ */
498
+ function capitalizeFirstWord(text) {
499
+ return text.length===0 ? '' : text.charAt(0).toUpperCase() + text.slice(1);
500
+ }
501
+
502
+
503
+ function formatResult(result) {
504
+ switch (result) {
505
+ case '1/2-1/2': return '\u00bd\u2013\u00bd';
506
+ case '1-0': return '1\u20130';
507
+ case '0-1': return '0\u20131';
508
+ }
509
+ }
510
+
511
+
512
+ function createSanitizer() {
513
+ return new HtmlSanitizer({
514
+ allowedTags: [ 'a', 'span', 'b', 'strong', 'i', 'em', 'mark', 'small', 'del', 'ins', 'sub', 'sup' ],
515
+ allowedAttributes: {
516
+ '*': [ 'class', 'id', 'title' ],
517
+ 'a': [ 'href', 'target' ]
518
+ }
519
+ });
520
+ }
521
+
522
+
523
+ function sanitizeHtml(text, sanitizer) {
524
+ if (!sanitizer) {
525
+ sanitizer = createSanitizer();
526
+ }
527
+ let result = sanitizer.parse(text);
528
+ return result ?? text;
529
+ }
530
+
531
+
532
+ /**
533
+ * Decompose the given string into piece symbol characters and sections of non piece symbol characters, and transform the piece symbols into
534
+ * React objects represented with the given chess font.
535
+ */
536
+ function figurineNotation(text, fontName) {
537
+ let result = [];
538
+ let beginOfText = 0;
539
+ let pieceSymbolIndex = 0;
540
+ for (let pos = 0; pos < text.length; ++pos) {
541
+ let currentChar = text.charAt(pos);
542
+ if (currentChar === 'K' || currentChar === 'Q' || currentChar === 'R' || currentChar === 'B' || currentChar === 'N' || currentChar === 'P') {
543
+ if (pos > beginOfText) {
544
+ result.push(text.substring(beginOfText, pos));
545
+ }
546
+ beginOfText = pos + 1;
547
+ let key = 'symbol-' + (pieceSymbolIndex++);
548
+ result.push(<span className={'kokopu-font-' + fontName} key={key}>{currentChar}</span>);
549
+ }
550
+ }
551
+ if (beginOfText < text.length) {
552
+ result.push(text.substring(beginOfText));
553
+ }
554
+ return result;
555
+ }
556
+
557
+
558
+ function getPreviousNodeId(currentNode) {
559
+ let previousNode = currentNode.previous();
560
+ if (previousNode) {
561
+ return previousNode.id();
562
+ }
563
+ else {
564
+ let parentNode = currentNode.parentVariation().parentNode();
565
+ return parentNode ? getPreviousNodeId(parentNode) : 'start';
566
+ }
567
+ }
568
+
569
+
570
+ function getNextNodeId(currentNode, isVariation) {
571
+ let nextNode = isVariation ? currentNode.first() : currentNode.next();
572
+ return nextNode ? nextNode.id() : false;
573
+ }
574
+
575
+
576
+ function getLastNodeId(currentNode, isVariation) {
577
+ currentNode = isVariation ? currentNode.first() : currentNode.next();
578
+ if (!currentNode) { // Ensure that the input node is not already the last one.
579
+ return false;
580
+ }
581
+ while (true) {
582
+ let nextNode = currentNode.next();
583
+ if (!nextNode) {
584
+ return currentNode.id();
585
+ }
586
+ currentNode = nextNode;
587
+ }
588
+ }
589
+
590
+
591
+ /**
592
+ * Try to interpret the given object as a chess game.
593
+ */
594
+ function parseGame(game, gameIndex) {
595
+ if (game instanceof kokopu.Game) {
596
+ return { error: false, game: game };
597
+ }
598
+ else if (game instanceof kokopu.Database || typeof game === 'string') {
599
+ try {
600
+ let result = game instanceof kokopu.Database ? game.game(gameIndex) : kokopu.pgnRead(game, gameIndex);
601
+ return { error: false, game: result };
602
+ }
603
+ catch (e) {
604
+ // istanbul ignore else
605
+ if (e instanceof kokopu.exception.InvalidPGN) {
606
+ return { error: true, message: e.message, text: e.pgn, errorIndex: e.index, lineNumber: e.lineNumber };
607
+ }
608
+ else {
609
+ throw e;
610
+ }
611
+ }
612
+ }
613
+ else {
614
+ return { error: true, message: i18n.INVALID_GAME_ATTRIBUTE_ERROR_MESSAGE };
615
+ }
616
+ }
@@ -1,7 +1,7 @@
1
1
  /******************************************************************************
2
2
  * *
3
3
  * This file is part of Kokopu-React, a JavaScript chess library. *
4
- * Copyright (C) 2021 Yoann Le Montagner <yo35 -at- melix.net> *
4
+ * Copyright (C) 2021-2022 Yoann Le Montagner <yo35 -at- melix.net> *
5
5
  * *
6
6
  * This program is free software: you can redistribute it and/or *
7
7
  * modify it under the terms of the GNU Lesser General Public License *
@@ -1,7 +1,7 @@
1
1
  /******************************************************************************
2
2
  * *
3
3
  * This file is part of Kokopu-React, a JavaScript chess library. *
4
- * Copyright (C) 2021 Yoann Le Montagner <yo35 -at- melix.net> *
4
+ * Copyright (C) 2021-2022 Yoann Le Montagner <yo35 -at- melix.net> *
5
5
  * *
6
6
  * This program is free software: you can redistribute it and/or *
7
7
  * modify it under the terms of the GNU Lesser General Public License *
@@ -23,9 +23,8 @@
23
23
  import PropTypes from 'prop-types';
24
24
  import React from 'react';
25
25
 
26
- import './css/label.css';
27
-
28
26
  import { MIN_SQUARE_SIZE, MAX_SQUARE_SIZE, sanitizeInteger, isValidSymbol } from './impl/util';
27
+ import TextSymbol from './impl/TextSymbol';
29
28
 
30
29
 
31
30
  /**
@@ -39,9 +38,7 @@ export default function TextMarkerIcon(props) {
39
38
  let viewBox = `0 0 ${size} ${size}`;
40
39
  return (
41
40
  <svg className="kokopu-textMarkerIcon" viewBox={viewBox} width={size} height={size}>
42
- <text className="kokopu-label" x={size / 2} y={size / 2} fill={props.color} style={{ 'fontSize': size }}>
43
- {props.symbol}
44
- </text>
41
+ <TextSymbol x={size / 2} y={size / 2} size={size} symbol={props.symbol} color={props.color} />
45
42
  </svg>
46
43
  );
47
44
  }
@@ -54,7 +51,10 @@ TextMarkerIcon.propTypes = {
54
51
  size: PropTypes.number.isRequired,
55
52
 
56
53
  /**
57
- * Symbol to represent on the icon. Must be either a letter (upper-case or lower-case) or a digit.
54
+ * Symbol to represent on the icon. Must be one of:
55
+ * - any letter from A to Z (either upper-case or lower-case),
56
+ * - any digit from 0 to 9,
57
+ * - any symbol code among `'plus'`, `'times'`, `'dot'`, or `'circle'`.
58
58
  */
59
59
  symbol: PropTypes.string.isRequired,
60
60
 
package/src/css/arrow.css CHANGED
@@ -1,7 +1,7 @@
1
1
  /******************************************************************************
2
2
  * *
3
3
  * This file is part of Kokopu-React, a JavaScript chess library. *
4
- * Copyright (C) 2021 Yoann Le Montagner <yo35 -at- melix.net> *
4
+ * Copyright (C) 2021-2022 Yoann Le Montagner <yo35 -at- melix.net> *
5
5
  * *
6
6
  * This program is free software: you can redistribute it and/or *
7
7
  * modify it under the terms of the GNU Lesser General Public License *
@@ -1,7 +1,7 @@
1
1
  /******************************************************************************
2
2
  * *
3
3
  * This file is part of Kokopu-React, a JavaScript chess library. *
4
- * Copyright (C) 2021 Yoann Le Montagner <yo35 -at- melix.net> *
4
+ * Copyright (C) 2021-2022 Yoann Le Montagner <yo35 -at- melix.net> *
5
5
  * *
6
6
  * This program is free software: you can redistribute it and/or *
7
7
  * modify it under the terms of the GNU Lesser General Public License *
@@ -22,6 +22,11 @@
22
22
 
23
23
  .kokopu-coordinate {
24
24
  fill: currentcolor;
25
+ font-style: normal;
26
+ font-family: sans-serif;
27
+ text-anchor: middle;
28
+ dominant-baseline: central;
29
+ letter-spacing: 0px;
25
30
  }
26
31
 
27
32
  .kokopu-annotation {