kokopu-react 1.5.3 → 1.6.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  ChangeLog
2
2
  =========
3
3
 
4
+ 1.6.0 (June 1, 2022)
5
+ --------------------
6
+ * Allow block-formatting HTML tags in PGN comments.
7
+ * Provide `formatMove` and `moveFormatter`.
8
+
9
+ 1.5.5 (May 18, 2022)
10
+ --------------------
11
+ * Upgrade dependencies.
12
+ * Remove dependency with security issue.
13
+
14
+ 1.5.4 (May 1, 2022)
15
+ -------------------
16
+ * Provide method `Chessboard#size(..)` to retrieve the width/height of a `Chessboard` component instance.
17
+
4
18
  1.5.3 (April 30, 2022)
5
19
  ----------------------
6
20
  * Add method `Movetext#focus()`.
package/README.md CHANGED
@@ -3,7 +3,7 @@ Kokopu-React
3
3
 
4
4
  <img align="right" width="96" height="96" src="graphics/logo.svg" />
5
5
 
6
- Kokopu-React is a [React](https://reactjs.org/)-based library to create and display chessboard components.
6
+ Kokopu-React is a [React](https://reactjs.org/)-based library to create and display chessboard and chess-related components.
7
7
  Kokopu-React is built on top of [Kokopu](https://www.npmjs.com/package/kokopu), a headless library that
8
8
  implements all the chess logic (game rules, parsing of [FEN](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
9
9
  and [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) formats...).
@@ -27,7 +27,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
27
27
 
28
28
  function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
29
29
 
30
- function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
30
+ function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
31
31
 
32
32
  function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
33
33
 
@@ -37,7 +37,7 @@ function _assertThisInitialized(self) { if (self === void 0) { throw new Referen
37
37
 
38
38
  function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
39
39
 
40
- function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
40
+ function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
41
41
 
42
42
  var ARROW_THICKNESS_FACTOR = 0.2;
43
43
  /**
@@ -59,7 +59,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
59
59
 
60
60
  function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
61
61
 
62
- function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
62
+ function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
63
63
 
64
64
  function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
65
65
 
@@ -69,7 +69,7 @@ function _assertThisInitialized(self) { if (self === void 0) { throw new Referen
69
69
 
70
70
  function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
71
71
 
72
- function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
72
+ function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
73
73
 
74
74
  var TURN_FLAG_SPACING_FACTOR = 0.1;
75
75
  var RANK_COORDINATE_WIDTH_FACTOR = 1;
@@ -984,6 +984,46 @@ var Chessboard = /*#__PURE__*/function (_React$Component) {
984
984
  value: function getArrowTipId(color) {
985
985
  return 'kokopu-arrowTip-' + color + '-' + this.arrowTipIdSuffix;
986
986
  }
987
+ /**
988
+ * Return the size of the chessboard, assuming it is built with the given attributes.
989
+ *
990
+ * @param {number} squareSize
991
+ * @param {boolean} coordinateVisible
992
+ * @param {{width:number, squareSize:number, coordinateVisible:boolean}[]} smallScreenLimits
993
+ * @returns {{width:number, height:number}}
994
+ * @public
995
+ */
996
+
997
+ }], [{
998
+ key: "size",
999
+ value: function size(squareSize, coordinateVisible, smallScreenLimits) {
1000
+ // Enforce small-screen limits, if any.
1001
+ if (typeof window !== 'undefined') {
1002
+ var squareSizeLimit = computeSmallScreenLimits('squareSize', smallScreenLimits, window.innerWidth);
1003
+ var coordinateVisibleLimit = computeSmallScreenLimits('coordinateVisible', smallScreenLimits, window.innerWidth);
1004
+
1005
+ if (!isNaN(squareSizeLimit)) {
1006
+ squareSize = (0, _util.sanitizeInteger)(squareSizeLimit, _util.MIN_SQUARE_SIZE, squareSize);
1007
+ }
1008
+
1009
+ coordinateVisible = coordinateVisible && (coordinateVisibleLimit === undefined || coordinateVisibleLimit);
1010
+ } // Compute the dimensions.
1011
+
1012
+
1013
+ var width = 9 * squareSize + Math.round(TURN_FLAG_SPACING_FACTOR * squareSize);
1014
+ var height = 8 * squareSize;
1015
+
1016
+ if (coordinateVisible) {
1017
+ var fontSize = computeCoordinateFontSize(squareSize);
1018
+ width += Math.round(RANK_COORDINATE_WIDTH_FACTOR * fontSize);
1019
+ height += Math.round(FILE_COORDINATE_HEIGHT_FACTOR * fontSize);
1020
+ }
1021
+
1022
+ return {
1023
+ width: width,
1024
+ height: height
1025
+ };
1026
+ }
987
1027
  /**
988
1028
  * Return the maximum square size that would allow the chessboard to fit in a rectangle of size `width x height`.
989
1029
  *
@@ -995,7 +1035,7 @@ var Chessboard = /*#__PURE__*/function (_React$Component) {
995
1035
  * @public
996
1036
  */
997
1037
 
998
- }], [{
1038
+ }, {
999
1039
  key: "adaptSquareSize",
1000
1040
  value: function adaptSquareSize(width, height, coordinateVisible, smallScreenLimits) {
1001
1041
  var maxSquareSize = _util.MAX_SQUARE_SIZE; // Enforce small-screen limits, if any.
@@ -19,9 +19,9 @@ var _Chessboard = _interopRequireDefault(require("./Chessboard"));
19
19
 
20
20
  var _ErrorBox = _interopRequireDefault(require("./ErrorBox"));
21
21
 
22
- var _i18n = _interopRequireDefault(require("./i18n"));
22
+ var _formatmove = require("./formatmove");
23
23
 
24
- require("./css/fonts.css");
24
+ var _i18n = _interopRequireDefault(require("./i18n"));
25
25
 
26
26
  require("./css/movetext.css");
27
27
 
@@ -49,7 +49,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
49
49
 
50
50
  function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
51
51
 
52
- function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
52
+ function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
53
53
 
54
54
  function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
55
55
 
@@ -59,7 +59,7 @@ function _assertThisInitialized(self) { if (self === void 0) { throw new Referen
59
59
 
60
60
  function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
61
61
 
62
- function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
62
+ function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
63
63
 
64
64
  /**
65
65
  * 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...).
@@ -96,7 +96,7 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
96
96
 
97
97
  return /*#__PURE__*/_react["default"].createElement("div", {
98
98
  className: "kokopu-movetext"
99
- }, this.renderHeaders(info.game), this.renderBody(info.game), this.renderFocusField());
99
+ }, this.renderHeaders(info.game), this.renderBody(info.game), this.renderFocusField(info.game));
100
100
  }
101
101
  }, {
102
102
  key: "renderHeaders",
@@ -233,7 +233,7 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
233
233
  }
234
234
  }, {
235
235
  key: "renderFocusField",
236
- value: function renderFocusField() {
236
+ value: function renderFocusField(game) {
237
237
  var _this2 = this;
238
238
 
239
239
  if (this.props.interactionMode !== 'selectMove') {
@@ -247,14 +247,15 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
247
247
  href: "#",
248
248
  ref: this.focusFieldRef,
249
249
  onKeyDown: function onKeyDown(evt) {
250
- return _this2.handleKeyDownInFocusField(evt);
250
+ return _this2.handleKeyDownInFocusField(evt, game);
251
251
  }
252
252
  }));
253
253
  }
254
254
  }, {
255
255
  key: "renderBody",
256
256
  value: function renderBody(game) {
257
- return this.renderVariation(this.getNotationTextBuilder(), game.mainVariation(), 'main-variation', true, game.result());
257
+ var notationTextBuilder = (0, _formatmove.moveFormatter)(this.props.pieceSymbols);
258
+ return this.renderVariation(notationTextBuilder, game.mainVariation(), 'main-variation', true, game.result());
258
259
  }
259
260
  /**
260
261
  * Render the given variation and its sub-variations, recursively.
@@ -441,10 +442,10 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
441
442
  key: "renderComment",
442
443
  value: function renderComment(node, comment, isVariation) {
443
444
  var content;
445
+ var sanitizer = createSanitizer(true);
444
446
 
445
447
  if (this.props.diagramVisible && comment.includes('[#]')) {
446
448
  content = [];
447
- var sanitizer = createSanitizer();
448
449
  var isFirstPart = true;
449
450
 
450
451
  var _iterator2 = _createForOfIteratorHelper(comment.split('[#]').entries()),
@@ -491,7 +492,7 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
491
492
  _iterator2.f();
492
493
  }
493
494
  } else {
494
- content = sanitizeHtml(comment);
495
+ content = sanitizeHtml(comment, sanitizer);
495
496
  }
496
497
 
497
498
  var key = isVariation ? 'initial-comment' : node.fullMoveNumber() + node.moveColor() + '-comment';
@@ -522,7 +523,7 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
522
523
  }
523
524
  }, {
524
525
  key: "handleKeyDownInFocusField",
525
- value: function handleKeyDownInFocusField(evt) {
526
+ value: function handleKeyDownInFocusField(evt, game) {
526
527
  if (evt.key !== 'Home' && evt.key !== 'ArrowLeft' && evt.key !== 'ArrowRight' && evt.key !== 'End') {
527
528
  return;
528
529
  }
@@ -533,9 +534,6 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
533
534
  return;
534
535
  }
535
536
 
536
- var _parseGame = parseGame(this.props.game, this.props.gameIndex),
537
- game = _parseGame.game;
538
-
539
537
  var nodeId = undefined;
540
538
  var evtOrigin = '';
541
539
 
@@ -567,40 +565,6 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
567
565
  this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
568
566
  }
569
567
  }
570
- /**
571
- * Return the square at the given location.
572
- */
573
-
574
- }, {
575
- key: "getNotationTextBuilder",
576
- value: function getNotationTextBuilder() {
577
- var pieceSymbols = this.props.pieceSymbols;
578
-
579
- if (pieceSymbols === 'localized') {
580
- var mapping = _i18n["default"].PIECE_SYMBOLS;
581
- return function (notation) {
582
- return notation.replace(/[KQRBNP]/g, function (match) {
583
- return mapping[match];
584
- });
585
- };
586
- } else if (pieceSymbols === 'figurines') {
587
- return function (notation) {
588
- return figurineNotation(notation, 'alpha');
589
- };
590
- } else if (pieceSymbols !== 'native' && pieceSymbols && ['K', 'Q', 'R', 'B', 'N', 'P'].every(function (p) {
591
- return typeof pieceSymbols[p] === 'string';
592
- })) {
593
- return function (notation) {
594
- return notation.replace(/[KQRBNP]/g, function (match) {
595
- return pieceSymbols[match];
596
- });
597
- };
598
- } else {
599
- return function (notation) {
600
- return notation;
601
- };
602
- }
603
- }
604
568
  /**
605
569
  * Set the focus to the current component.
606
570
  *
@@ -849,58 +813,50 @@ function formatResult(result) {
849
813
  }
850
814
  }
851
815
 
852
- function createSanitizer() {
816
+ function createSanitizer(withBlockTags) {
817
+ var allowedTags = ['span', // general purpose
818
+ 'a', // links
819
+ 'b', 'strong', 'i', 'em', // bold, italic
820
+ 'del', 'ins', 's', 'u', // underline, strikethrough
821
+ 'sub', 'sup', // subscript, superscript
822
+ 'abbr', // acronym (typically associated to a title=".." tooltip)
823
+ 'q', 'cite', // quotation and title of a work
824
+ 'mark', // highlighted text
825
+ 'small' // smaller text
826
+ ];
827
+ var allowedAttributes = {
828
+ '*': ['class', 'id', 'title'],
829
+ 'a': ['href', 'target']
830
+ };
831
+
832
+ if (withBlockTags) {
833
+ allowedTags = allowedTags.concat(['div', // general purpose
834
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', // headings
835
+ 'p', 'br', // paragraph and line break
836
+ 'blockquote', // long quotation
837
+ 'img', // image
838
+ 'pre', // preformatted text
839
+ 'ul', 'ol', 'li' // lists
840
+ ]);
841
+ Object.assign(allowedAttributes, {
842
+ 'img': ['alt', 'src', 'height', 'width']
843
+ });
844
+ }
845
+
853
846
  return new _HtmlSanitizer["default"]({
854
- allowedTags: ['a', 'span', 'b', 'strong', 'i', 'em', 'mark', 'small', 'del', 'ins', 'sub', 'sup'],
855
- allowedAttributes: {
856
- '*': ['class', 'id', 'title'],
857
- 'a': ['href', 'target']
858
- }
847
+ allowedTags: allowedTags,
848
+ allowedAttributes: allowedAttributes
859
849
  });
860
850
  }
861
851
 
862
852
  function sanitizeHtml(text, sanitizer) {
863
853
  if (!sanitizer) {
864
- sanitizer = createSanitizer();
854
+ sanitizer = createSanitizer(false);
865
855
  }
866
856
 
867
857
  var result = sanitizer.parse(text);
868
858
  return result !== null && result !== void 0 ? result : text;
869
859
  }
870
- /**
871
- * Decompose the given string into piece symbol characters and sections of non piece symbol characters, and transform the piece symbols into
872
- * React objects represented with the given chess font.
873
- */
874
-
875
-
876
- function figurineNotation(text, fontName) {
877
- var result = [];
878
- var beginOfText = 0;
879
- var pieceSymbolIndex = 0;
880
-
881
- for (var pos = 0; pos < text.length; ++pos) {
882
- var currentChar = text.charAt(pos);
883
-
884
- if (currentChar === 'K' || currentChar === 'Q' || currentChar === 'R' || currentChar === 'B' || currentChar === 'N' || currentChar === 'P') {
885
- if (pos > beginOfText) {
886
- result.push(text.substring(beginOfText, pos));
887
- }
888
-
889
- beginOfText = pos + 1;
890
- var key = 'symbol-' + pieceSymbolIndex++;
891
- result.push( /*#__PURE__*/_react["default"].createElement("span", {
892
- className: 'kokopu-font-' + fontName,
893
- key: key
894
- }, currentChar));
895
- }
896
- }
897
-
898
- if (beginOfText < text.length) {
899
- result.push(text.substring(beginOfText));
900
- }
901
-
902
- return result;
903
- }
904
860
  /**
905
861
  * Try to interpret the given object as a chess game.
906
862
  */
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.formatMove = formatMove;
7
+ exports.moveFormatter = moveFormatter;
8
+
9
+ var _react = _interopRequireDefault(require("react"));
10
+
11
+ var _i18n = _interopRequireDefault(require("./i18n"));
12
+
13
+ require("./css/fonts.css");
14
+
15
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
16
+
17
+ /******************************************************************************
18
+ * *
19
+ * This file is part of Kokopu-React, a JavaScript chess library. *
20
+ * Copyright (C) 2021-2022 Yoann Le Montagner <yo35 -at- melix.net> *
21
+ * *
22
+ * This program is free software: you can redistribute it and/or *
23
+ * modify it under the terms of the GNU Lesser General Public License *
24
+ * as published by the Free Software Foundation, either version 3 of *
25
+ * the License, or (at your option) any later version. *
26
+ * *
27
+ * This program is distributed in the hope that it will be useful, *
28
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
29
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
30
+ * GNU Lesser General Public License for more details. *
31
+ * *
32
+ * You should have received a copy of the GNU Lesser General *
33
+ * Public License along with this program. If not, see *
34
+ * <http://www.gnu.org/licenses/>. *
35
+ * *
36
+ ******************************************************************************/
37
+
38
+ /**
39
+ * Render the given SAN notation according to the given piece symbol style.
40
+ *
41
+ * @param {string|object} pieceSymbols See {@link moveFormatter}.
42
+ * @param {string} notation SAN notation.
43
+ * @returns {string|React.ReactFragment}
44
+ */
45
+ function formatMove(pieceSymbols, notation) {
46
+ var formatter = moveFormatter(pieceSymbols);
47
+ return formatter(notation);
48
+ }
49
+ /**
50
+ * Return a callback capable of rendering a SAN notation using the given piece symbol style.
51
+ *
52
+ * @param {string|object} pieceSymbols `'native'`, `'localized'`, `'figurines'`, or an object defining a string-valued properties
53
+ * for each English piece symbol.
54
+ * @returns {function(string)} Callback returning either a string or a React fragment.
55
+ */
56
+
57
+
58
+ function moveFormatter(pieceSymbols) {
59
+ if (pieceSymbols === 'localized') {
60
+ var mapping = _i18n["default"].PIECE_SYMBOLS;
61
+ return function (notation) {
62
+ return notation.replace(/[KQRBNP]/g, function (match) {
63
+ return mapping[match];
64
+ });
65
+ };
66
+ } else if (pieceSymbols === 'figurines') {
67
+ return function (notation) {
68
+ return figurineNotation('alpha', notation);
69
+ };
70
+ } else if (pieceSymbols !== 'native' && pieceSymbols && ['K', 'Q', 'R', 'B', 'N', 'P'].every(function (p) {
71
+ return typeof pieceSymbols[p] === 'string';
72
+ })) {
73
+ return function (notation) {
74
+ return notation.replace(/[KQRBNP]/g, function (match) {
75
+ return pieceSymbols[match];
76
+ });
77
+ };
78
+ } else {
79
+ return function (notation) {
80
+ return notation;
81
+ };
82
+ }
83
+ }
84
+ /**
85
+ * Decompose the given string into piece symbol characters and sections of non piece symbol characters, and transform the piece symbols into
86
+ * React objects represented with the given chess font.
87
+ *
88
+ * @ignore
89
+ */
90
+
91
+
92
+ function figurineNotation(fontName, text) {
93
+ var result = [];
94
+ var beginOfText = 0;
95
+ var pieceSymbolIndex = 0;
96
+
97
+ for (var pos = 0; pos < text.length; ++pos) {
98
+ var currentChar = text.charAt(pos);
99
+
100
+ if (currentChar === 'K' || currentChar === 'Q' || currentChar === 'R' || currentChar === 'B' || currentChar === 'N' || currentChar === 'P') {
101
+ if (pos > beginOfText) {
102
+ result.push(text.substring(beginOfText, pos));
103
+ }
104
+
105
+ beginOfText = pos + 1;
106
+ var key = 'symbol-' + pieceSymbolIndex++;
107
+ result.push( /*#__PURE__*/_react["default"].createElement("span", {
108
+ className: 'kokopu-font-' + fontName,
109
+ key: key
110
+ }, currentChar));
111
+ }
112
+ }
113
+
114
+ if (beginOfText < text.length) {
115
+ result.push(text.substring(beginOfText));
116
+ }
117
+
118
+ return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, result);
119
+ }
@@ -35,9 +35,14 @@ function _defineProperties(target, props) { for (var i = 0; i < props.length; i+
35
35
 
36
36
  function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
37
37
 
38
+ /**
39
+ * Those tags cannot have any children.
40
+ */
41
+ var NO_CHILDREN_TAGS = new Set(['br', 'hr', 'img']);
38
42
  /**
39
43
  * HTML sanitizer: parse HTML string, keeping only the allowed HTML nodes, and the corresponding React object.
40
44
  */
45
+
41
46
  var HtmlSanitizer = /*#__PURE__*/function () {
42
47
  function HtmlSanitizer(_ref) {
43
48
  var allowedTags = _ref.allowedTags,
@@ -101,7 +106,7 @@ var HtmlSanitizer = /*#__PURE__*/function () {
101
106
  if (node.type === 'text') {
102
107
  return node.data;
103
108
  } else if (node.type === 'tag') {
104
- var children = node.children.map(function (child) {
109
+ var children = NO_CHILDREN_TAGS.has(node.name) ? undefined : node.children.map(function (child) {
105
110
  return _this2._processNode(child);
106
111
  });
107
112
 
@@ -111,9 +116,9 @@ var HtmlSanitizer = /*#__PURE__*/function () {
111
116
  attributes['key'] = this._allocateKey();
112
117
  return /*#__PURE__*/_react["default"].createElement(node.name, attributes, children);
113
118
  } else {
114
- return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, {
119
+ return children ? /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, {
115
120
  key: this._allocateKey()
116
- }, children);
121
+ }, children) : undefined;
117
122
  }
118
123
  } else {
119
124
  return undefined;
package/dist/lib/index.js CHANGED
@@ -57,12 +57,24 @@ Object.defineProperty(exports, "flattenTextMarkers", {
57
57
  return _markers.flattenTextMarkers;
58
58
  }
59
59
  });
60
+ Object.defineProperty(exports, "formatMove", {
61
+ enumerable: true,
62
+ get: function get() {
63
+ return _formatmove.formatMove;
64
+ }
65
+ });
60
66
  Object.defineProperty(exports, "i18n", {
61
67
  enumerable: true,
62
68
  get: function get() {
63
69
  return _i18n["default"];
64
70
  }
65
71
  });
72
+ Object.defineProperty(exports, "moveFormatter", {
73
+ enumerable: true,
74
+ get: function get() {
75
+ return _formatmove.moveFormatter;
76
+ }
77
+ });
66
78
  Object.defineProperty(exports, "parseArrowMarkers", {
67
79
  enumerable: true,
68
80
  get: function get() {
@@ -84,6 +96,8 @@ Object.defineProperty(exports, "parseTextMarkers", {
84
96
 
85
97
  var _markers = require("./markers");
86
98
 
99
+ var _formatmove = require("./formatmove");
100
+
87
101
  var _i18n = _interopRequireDefault(require("./i18n"));
88
102
 
89
103
  var _Chessboard = _interopRequireDefault(require("./Chessboard"));
package/doc_src/home.md CHANGED
@@ -1,4 +1,4 @@
1
- Kokopu-React is a [React](https://reactjs.org/)-based library to create and display chessboard components.
1
+ Kokopu-React is a [React](https://reactjs.org/)-based library to create and display chessboard and chess-related components.
2
2
  Kokopu-React is built on top of [Kokopu](https://www.npmjs.com/package/kokopu), a headless library that
3
3
  implements all the chess logic (game rules, parsing of [FEN](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
4
4
  and [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) formats...).
@@ -85,7 +85,7 @@ probably to assure himself that the combination was sound and that he had a forc
85
85
  [Date "????.??.??"]
86
86
  [Round "<strong>3</strong>.2"]
87
87
  [White "1<sup>st</sup> <em>player</em>"]
88
- [Black "2<sub>nd</sub> <em>player</em>"]
88
+ [Black "2<sub>nd</sub> <i>player</i>"]
89
89
  [Result "*"]
90
90
  [WhiteElo "<ins>2800</ins>"]
91
91
  [BlackElo "<del>1010</del>"]
@@ -104,7 +104,13 @@ probably to assure himself that the combination was sound and that he had a forc
104
104
  [Black "?"]
105
105
  [Result "*"]
106
106
 
107
- {I'm a <strong><ins>comment</ins></strong> with some <em class="myClass">HTML content</em> in it.}
107
+ {I'm a <strong><ins>comment</ins></strong> with some <em class="myClass">HTML content</em> in it.<br/>
108
+ Lists are supported:
109
+ <ul>
110
+ <li>1<sup>st</sup> point</li>
111
+ <li>2<sup>nd</sup> point</li>
112
+ </ul>
113
+ ... and so are images: <img src="../smiley.png" alt="smiley" />}
108
114
 
109
115
  *
110
116
 
@@ -119,9 +125,9 @@ probably to assure himself that the combination was sound and that he had a forc
119
125
  [Result "*"]
120
126
 
121
127
  {
122
- Using <div class="myClass">div tags</div> is not allowed.
123
- ... but <div class="myClass"><em>legit HTML elements</em> within forbidden HTML tags</div> are rendered.
124
- Using <span style="color: cyan;">style attributes</span> is not allowed.
128
+ Using <form class="myClass">form-related tags</form> is not allowed.<br/>
129
+ ... but <button class="myClass"><em>legit HTML elements</em> within forbidden HTML tags</button> are rendered.<br/>
130
+ Using <span style="color: cyan;">style attributes</span> is not allowed.<br/>
125
131
  Using scripts is not allowed. <script>alert('You should not see this either.');</script>
126
132
  }
127
133
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kokopu-react",
3
- "version": "1.5.3",
4
- "description": "A React-based library to create and display chessboard components.",
3
+ "version": "1.6.0",
4
+ "description": "A React-based library to create and display chessboard and chess-related components.",
5
5
  "keywords": [
6
6
  "chess",
7
7
  "react",
@@ -29,41 +29,40 @@
29
29
  ".nycrc.yml"
30
30
  ],
31
31
  "dependencies": {
32
- "htmlparser2": "^7.2.0",
33
- "kokopu": "^2.7.0",
32
+ "htmlparser2": "^8.0.1",
33
+ "kokopu": "^2.8.0",
34
34
  "prop-types": "^15.8.1",
35
35
  "react": "^17.0.2",
36
36
  "react-draggable": "^4.4.5",
37
37
  "react-motion": "^0.5.2"
38
38
  },
39
39
  "devDependencies": {
40
- "@babel/cli": "^7.17.6",
41
- "@babel/core": "^7.17.9",
42
- "@babel/preset-env": "^7.16.11",
43
- "@babel/preset-react": "^7.16.7",
40
+ "@babel/cli": "^7.17.10",
41
+ "@babel/core": "^7.18.2",
42
+ "@babel/preset-env": "^7.18.2",
43
+ "@babel/preset-react": "^7.17.12",
44
44
  "@emotion/react": "^11.9.0",
45
45
  "@emotion/styled": "^11.8.1",
46
- "@mui/material": "^5.6.3",
46
+ "@mui/material": "^5.8.2",
47
47
  "babel-loader": "^8.2.5",
48
48
  "babel-plugin-istanbul": "^6.1.1",
49
- "copy-webpack-plugin": "^10.2.4",
49
+ "copy-webpack-plugin": "^11.0.0",
50
50
  "coveralls": "^3.1.1",
51
51
  "css-loader": "^6.7.1",
52
- "eslint": "^8.14.0",
53
- "eslint-plugin-react": "^7.29.4",
52
+ "eslint": "^8.16.0",
53
+ "eslint-plugin-react": "^7.30.0",
54
54
  "html-webpack-plugin": "^5.5.0",
55
55
  "img-diff-js": "^0.5.2",
56
- "mocha": "^9.2.2",
56
+ "mocha": "^10.0.0",
57
57
  "null-loader": "^4.0.1",
58
58
  "nyc": "^15.1.0",
59
- "prompt": "^1.3.0",
60
59
  "react-dom": "^17.0.2",
61
60
  "react-styleguidist": "^11.2.0",
62
61
  "selenium-webdriver": "^4.1.2",
63
- "ssh2-sftp-client": "^8.0.0",
62
+ "ssh2-sftp-client": "^8.1.0",
64
63
  "style-loader": "^3.3.1",
65
64
  "unit.js": "^2.1.1",
66
- "webpack": "^5.72.0",
65
+ "webpack": "^5.72.1",
67
66
  "webpack-cli": "^4.9.2"
68
67
  },
69
68
  "scripts": {
@@ -39,7 +39,10 @@ var plugins = items.map(item => new HtmlWebpackPlugin({
39
39
  filename: item + '/index.html',
40
40
  }));
41
41
  plugins.push(new CopyWebpackPlugin({
42
- patterns: [ { from: './graphic_test_app/common/heartbeat.txt', to: 'heartbeat.txt' } ],
42
+ patterns: [
43
+ { from: './graphic_test_app/common/heartbeat.txt', to: 'heartbeat.txt' },
44
+ { from: './graphic_test_app/common/smiley.png', to: 'smiley.png' },
45
+ ],
43
46
  }));
44
47
 
45
48
  module.exports = {
package/src/Chessboard.js CHANGED
@@ -623,6 +623,38 @@ export default class Chessboard extends React.Component {
623
623
  return 'kokopu-arrowTip-' + color + '-' + this.arrowTipIdSuffix;
624
624
  }
625
625
 
626
+ /**
627
+ * Return the size of the chessboard, assuming it is built with the given attributes.
628
+ *
629
+ * @param {number} squareSize
630
+ * @param {boolean} coordinateVisible
631
+ * @param {{width:number, squareSize:number, coordinateVisible:boolean}[]} smallScreenLimits
632
+ * @returns {{width:number, height:number}}
633
+ * @public
634
+ */
635
+ static size(squareSize, coordinateVisible, smallScreenLimits) {
636
+
637
+ // Enforce small-screen limits, if any.
638
+ if (typeof window !== 'undefined') {
639
+ let squareSizeLimit = computeSmallScreenLimits('squareSize', smallScreenLimits, window.innerWidth);
640
+ let coordinateVisibleLimit = computeSmallScreenLimits('coordinateVisible', smallScreenLimits, window.innerWidth);
641
+ if (!isNaN(squareSizeLimit)) {
642
+ squareSize = sanitizeInteger(squareSizeLimit, MIN_SQUARE_SIZE, squareSize);
643
+ }
644
+ coordinateVisible = coordinateVisible && (coordinateVisibleLimit === undefined || coordinateVisibleLimit);
645
+ }
646
+
647
+ // Compute the dimensions.
648
+ let width = 9 * squareSize + Math.round(TURN_FLAG_SPACING_FACTOR * squareSize);
649
+ let height = 8 * squareSize;
650
+ if (coordinateVisible) {
651
+ let fontSize = computeCoordinateFontSize(squareSize);
652
+ width += Math.round(RANK_COORDINATE_WIDTH_FACTOR * fontSize);
653
+ height += Math.round(FILE_COORDINATE_HEIGHT_FACTOR * fontSize);
654
+ }
655
+ return { width: width, height: height };
656
+ }
657
+
626
658
  /**
627
659
  * Return the maximum square size that would allow the chessboard to fit in a rectangle of size `width x height`.
628
660
  *
package/src/Movetext.js CHANGED
@@ -27,9 +27,9 @@ import kokopu from 'kokopu';
27
27
  import HtmlSanitizer from './impl/HtmlSanitizer';
28
28
  import Chessboard from './Chessboard';
29
29
  import ErrorBox from './ErrorBox';
30
+ import { moveFormatter } from './formatmove';
30
31
  import i18n from './i18n';
31
32
 
32
- import './css/fonts.css';
33
33
  import './css/movetext.css';
34
34
 
35
35
 
@@ -52,7 +52,7 @@ export default class Movetext extends React.Component {
52
52
  <div className="kokopu-movetext">
53
53
  {this.renderHeaders(info.game)}
54
54
  {this.renderBody(info.game)}
55
- {this.renderFocusField()}
55
+ {this.renderFocusField(info.game)}
56
56
  </div>
57
57
  );
58
58
  }
@@ -132,19 +132,20 @@ export default class Movetext extends React.Component {
132
132
  return <div className="kokopu-header-annotator" key="annotator">{sanitizeHtml(annotator)}</div>;
133
133
  }
134
134
 
135
- renderFocusField() {
135
+ renderFocusField(game) {
136
136
  if (this.props.interactionMode !== 'selectMove') {
137
137
  return undefined;
138
138
  }
139
139
  return (
140
140
  <div className="kokopu-focusFieldContainer">
141
- <a className="kokopu-focusField" href="#" ref={this.focusFieldRef} onKeyDown={evt => this.handleKeyDownInFocusField(evt)}></a>
141
+ <a className="kokopu-focusField" href="#" ref={this.focusFieldRef} onKeyDown={evt => this.handleKeyDownInFocusField(evt, game)}></a>
142
142
  </div>
143
143
  );
144
144
  }
145
145
 
146
146
  renderBody(game) {
147
- return this.renderVariation(this.getNotationTextBuilder(), game.mainVariation(), 'main-variation', true, game.result());
147
+ let notationTextBuilder = moveFormatter(this.props.pieceSymbols);
148
+ return this.renderVariation(notationTextBuilder, game.mainVariation(), 'main-variation', true, game.result());
148
149
  }
149
150
 
150
151
  /**
@@ -284,9 +285,9 @@ export default class Movetext extends React.Component {
284
285
  */
285
286
  renderComment(node, comment, isVariation) {
286
287
  let content;
288
+ let sanitizer = createSanitizer(true);
287
289
  if (this.props.diagramVisible && comment.includes('[#]')) {
288
290
  content = [];
289
- let sanitizer = createSanitizer();
290
291
  let isFirstPart = true;
291
292
  for (let [index, part] of comment.split('[#]').entries()) {
292
293
  if (!isFirstPart) {
@@ -309,7 +310,7 @@ export default class Movetext extends React.Component {
309
310
  }
310
311
  }
311
312
  else {
312
- content = sanitizeHtml(comment);
313
+ content = sanitizeHtml(comment, sanitizer);
313
314
  }
314
315
  let key = isVariation ? 'initial-comment' : node.fullMoveNumber() + node.moveColor() + '-comment';
315
316
  return node.isLongComment() ? <div className="kokopu-comment" key={key}>{content}</div> : <span className="kokopu-comment" key={key}>{content}</span>;
@@ -330,7 +331,7 @@ export default class Movetext extends React.Component {
330
331
  return comment ? comment : undefined;
331
332
  }
332
333
 
333
- handleKeyDownInFocusField(evt) {
334
+ handleKeyDownInFocusField(evt, game) {
334
335
  if (evt.key !== 'Home' && evt.key !== 'ArrowLeft' && evt.key !== 'ArrowRight' && evt.key !== 'End') {
335
336
  return;
336
337
  }
@@ -338,7 +339,6 @@ export default class Movetext extends React.Component {
338
339
  if (!this.props.selection) {
339
340
  return;
340
341
  }
341
- let { game } = parseGame(this.props.game, this.props.gameIndex);
342
342
  let nodeId = undefined;
343
343
  let evtOrigin = '';
344
344
  if (evt.key === 'Home') {
@@ -369,26 +369,6 @@ export default class Movetext extends React.Component {
369
369
  }
370
370
  }
371
371
 
372
- /**
373
- * Return the square at the given location.
374
- */
375
- getNotationTextBuilder() {
376
- let pieceSymbols = this.props.pieceSymbols;
377
- if (pieceSymbols === 'localized') {
378
- let mapping = i18n.PIECE_SYMBOLS;
379
- return notation => notation.replace(/[KQRBNP]/g, match => mapping[match]);
380
- }
381
- else if (pieceSymbols === 'figurines') {
382
- return notation => figurineNotation(notation, 'alpha');
383
- }
384
- else if (pieceSymbols !== 'native' && pieceSymbols && ['K', 'Q', 'R', 'B', 'N', 'P'].every(p => typeof pieceSymbols[p] === 'string')) {
385
- return notation => notation.replace(/[KQRBNP]/g, match => pieceSymbols[match]);
386
- }
387
- else {
388
- return notation => notation;
389
- }
390
- }
391
-
392
372
  /**
393
373
  * Set the focus to the current component.
394
374
  *
@@ -616,52 +596,49 @@ function formatResult(result) {
616
596
  }
617
597
 
618
598
 
619
- function createSanitizer() {
620
- return new HtmlSanitizer({
621
- allowedTags: [ 'a', 'span', 'b', 'strong', 'i', 'em', 'mark', 'small', 'del', 'ins', 'sub', 'sup' ],
622
- allowedAttributes: {
623
- '*': [ 'class', 'id', 'title' ],
624
- 'a': [ 'href', 'target' ]
625
- }
626
- });
599
+ function createSanitizer(withBlockTags) {
600
+ let allowedTags = [
601
+ 'span', // general purpose
602
+ 'a', // links
603
+ 'b', 'strong', 'i', 'em', // bold, italic
604
+ 'del', 'ins', 's', 'u', // underline, strikethrough
605
+ 'sub', 'sup', // subscript, superscript
606
+ 'abbr', // acronym (typically associated to a title=".." tooltip)
607
+ 'q', 'cite', // quotation and title of a work
608
+ 'mark', // highlighted text
609
+ 'small', // smaller text
610
+ ];
611
+ let allowedAttributes = {
612
+ '*': [ 'class', 'id', 'title' ],
613
+ 'a': [ 'href', 'target' ],
614
+ };
615
+ if (withBlockTags) {
616
+ allowedTags = allowedTags.concat([
617
+ 'div', // general purpose
618
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', // headings
619
+ 'p', 'br', // paragraph and line break
620
+ 'blockquote', // long quotation
621
+ 'img', // image
622
+ 'pre', // preformatted text
623
+ 'ul', 'ol', 'li', // lists
624
+ ]);
625
+ Object.assign(allowedAttributes, {
626
+ 'img': [ 'alt', 'src', 'height', 'width' ],
627
+ });
628
+ }
629
+ return new HtmlSanitizer({ allowedTags: allowedTags, allowedAttributes: allowedAttributes });
627
630
  }
628
631
 
629
632
 
630
633
  function sanitizeHtml(text, sanitizer) {
631
634
  if (!sanitizer) {
632
- sanitizer = createSanitizer();
635
+ sanitizer = createSanitizer(false);
633
636
  }
634
637
  let result = sanitizer.parse(text);
635
638
  return result ?? text;
636
639
  }
637
640
 
638
641
 
639
- /**
640
- * Decompose the given string into piece symbol characters and sections of non piece symbol characters, and transform the piece symbols into
641
- * React objects represented with the given chess font.
642
- */
643
- function figurineNotation(text, fontName) {
644
- let result = [];
645
- let beginOfText = 0;
646
- let pieceSymbolIndex = 0;
647
- for (let pos = 0; pos < text.length; ++pos) {
648
- let currentChar = text.charAt(pos);
649
- if (currentChar === 'K' || currentChar === 'Q' || currentChar === 'R' || currentChar === 'B' || currentChar === 'N' || currentChar === 'P') {
650
- if (pos > beginOfText) {
651
- result.push(text.substring(beginOfText, pos));
652
- }
653
- beginOfText = pos + 1;
654
- let key = 'symbol-' + (pieceSymbolIndex++);
655
- result.push(<span className={'kokopu-font-' + fontName} key={key}>{currentChar}</span>);
656
- }
657
- }
658
- if (beginOfText < text.length) {
659
- result.push(text.substring(beginOfText));
660
- }
661
- return result;
662
- }
663
-
664
-
665
642
  /**
666
643
  * Try to interpret the given object as a chess game.
667
644
  */
@@ -0,0 +1,91 @@
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 React from 'react';
24
+ import i18n from './i18n';
25
+
26
+ import './css/fonts.css';
27
+
28
+
29
+ /**
30
+ * Render the given SAN notation according to the given piece symbol style.
31
+ *
32
+ * @param {string|object} pieceSymbols See {@link moveFormatter}.
33
+ * @param {string} notation SAN notation.
34
+ * @returns {string|React.ReactFragment}
35
+ */
36
+ export function formatMove(pieceSymbols, notation) {
37
+ let formatter = moveFormatter(pieceSymbols);
38
+ return formatter(notation);
39
+ }
40
+
41
+
42
+ /**
43
+ * Return a callback capable of rendering a SAN notation using the given piece symbol style.
44
+ *
45
+ * @param {string|object} pieceSymbols `'native'`, `'localized'`, `'figurines'`, or an object defining a string-valued properties
46
+ * for each English piece symbol.
47
+ * @returns {function(string)} Callback returning either a string or a React fragment.
48
+ */
49
+ export function moveFormatter(pieceSymbols) {
50
+ if (pieceSymbols === 'localized') {
51
+ let mapping = i18n.PIECE_SYMBOLS;
52
+ return notation => notation.replace(/[KQRBNP]/g, match => mapping[match]);
53
+ }
54
+ else if (pieceSymbols === 'figurines') {
55
+ return notation => figurineNotation('alpha', notation);
56
+ }
57
+ else if (pieceSymbols !== 'native' && pieceSymbols && ['K', 'Q', 'R', 'B', 'N', 'P'].every(p => typeof pieceSymbols[p] === 'string')) {
58
+ return notation => notation.replace(/[KQRBNP]/g, match => pieceSymbols[match]);
59
+ }
60
+ else {
61
+ return notation => notation;
62
+ }
63
+ }
64
+
65
+
66
+ /**
67
+ * Decompose the given string into piece symbol characters and sections of non piece symbol characters, and transform the piece symbols into
68
+ * React objects represented with the given chess font.
69
+ *
70
+ * @ignore
71
+ */
72
+ function figurineNotation(fontName, text) {
73
+ let result = [];
74
+ let beginOfText = 0;
75
+ let pieceSymbolIndex = 0;
76
+ for (let pos = 0; pos < text.length; ++pos) {
77
+ let currentChar = text.charAt(pos);
78
+ if (currentChar === 'K' || currentChar === 'Q' || currentChar === 'R' || currentChar === 'B' || currentChar === 'N' || currentChar === 'P') {
79
+ if (pos > beginOfText) {
80
+ result.push(text.substring(beginOfText, pos));
81
+ }
82
+ beginOfText = pos + 1;
83
+ let key = 'symbol-' + (pieceSymbolIndex++);
84
+ result.push(<span className={'kokopu-font-' + fontName} key={key}>{currentChar}</span>);
85
+ }
86
+ }
87
+ if (beginOfText < text.length) {
88
+ result.push(text.substring(beginOfText));
89
+ }
90
+ return <>{result}</>;
91
+ }
@@ -24,6 +24,12 @@ import React from 'react';
24
24
  import * as htmlparser2 from 'htmlparser2';
25
25
 
26
26
 
27
+ /**
28
+ * Those tags cannot have any children.
29
+ */
30
+ const NO_CHILDREN_TAGS = new Set([ 'br', 'hr', 'img' ]);
31
+
32
+
27
33
  /**
28
34
  * HTML sanitizer: parse HTML string, keeping only the allowed HTML nodes, and the corresponding React object.
29
35
  */
@@ -74,14 +80,14 @@ export default class HtmlSanitizer {
74
80
  return node.data;
75
81
  }
76
82
  else if (node.type === 'tag') {
77
- let children = node.children.map(child => this._processNode(child));
83
+ let children = NO_CHILDREN_TAGS.has(node.name) ? undefined : node.children.map(child => this._processNode(child));
78
84
  if (this._allowedTags.has(node.name)) {
79
85
  let attributes = this._filterAttributes(node.name, node.attribs);
80
86
  attributes['key'] = this._allocateKey();
81
87
  return React.createElement(node.name, attributes, children);
82
88
  }
83
89
  else {
84
- return <React.Fragment key={this._allocateKey()}>{children}</React.Fragment>;
90
+ return children ? <React.Fragment key={this._allocateKey()}>{children}</React.Fragment> : undefined;
85
91
  }
86
92
  }
87
93
  else {
package/src/index.js CHANGED
@@ -21,6 +21,7 @@
21
21
 
22
22
 
23
23
  export { flattenSquareMarkers, parseSquareMarkers, flattenTextMarkers, parseTextMarkers, flattenArrowMarkers, parseArrowMarkers } from './markers';
24
+ export { formatMove, moveFormatter } from './formatmove';
24
25
  export { default as i18n } from './i18n';
25
26
  export { default as Chessboard } from './Chessboard';
26
27
  export { default as ErrorBox } from './ErrorBox';
@@ -24,19 +24,33 @@ let { Chessboard } = require('../build/test_headless/index');
24
24
  let test = require('unit.js');
25
25
 
26
26
 
27
+ function testAdaptSquareSize(expectedSquareSize, width, height, coordinateVisible, smallScreenLimits) {
28
+ test.value(Chessboard.adaptSquareSize(width, height, coordinateVisible, smallScreenLimits)).is(expectedSquareSize);
29
+
30
+ let actualSize = Chessboard.size(expectedSquareSize, coordinateVisible, smallScreenLimits);
31
+ test.value(actualSize.width <= width && actualSize.height <= height).isTrue();
32
+ }
33
+
34
+ function testAdaptSquareSizeWithIncrement(expectedSquareSize, width, height, coordinateVisible, smallScreenLimits) {
35
+ testAdaptSquareSize(expectedSquareSize, width, height, coordinateVisible, smallScreenLimits);
36
+
37
+ let actualSizeIncremented = Chessboard.size(expectedSquareSize + 1, coordinateVisible, smallScreenLimits);
38
+ test.value(actualSizeIncremented.width > width || actualSizeIncremented.height > height).isTrue();
39
+ }
40
+
27
41
  describe('Adapt square-size', () => {
28
42
 
29
43
  it('Very small', () => { test.value(Chessboard.adaptSquareSize(10, 10, false)).is(Chessboard.minSquareSize()); });
30
44
  it('Very large', () => { test.value(Chessboard.adaptSquareSize(9999, 9999, true)).is(Chessboard.maxSquareSize()); });
31
45
 
32
- it('Size 185x300 with coordinates', () => { test.value(Chessboard.adaptSquareSize(185, 300, true)).is(19); });
33
- it('Size 185x300 without coordinates', () => { test.value(Chessboard.adaptSquareSize(185, 300, false)).is(20); });
34
- it('Size 300x200 with coordinates', () => { test.value(Chessboard.adaptSquareSize(300, 200, true)).is(23); });
35
- it('Size 300x200 without coordinates', () => { test.value(Chessboard.adaptSquareSize(300, 200, false)).is(25); });
36
- it('Size 375x500 with coordinates', () => { test.value(Chessboard.adaptSquareSize(375, 500, true)).is(40); });
37
- it('Size 375x500 without coordinates', () => { test.value(Chessboard.adaptSquareSize(375, 500, false)).is(41); });
38
- it('Size 600x450 with coordinates', () => { test.value(Chessboard.adaptSquareSize(600, 450, true)).is(54); });
39
- it('Size 600x450 without coordinates', () => { test.value(Chessboard.adaptSquareSize(600, 450, false)).is(56); });
46
+ it('Size 185x300 with coordinates', () => testAdaptSquareSizeWithIncrement(19, 185, 300, true));
47
+ it('Size 185x300 without coordinates', () => testAdaptSquareSizeWithIncrement(20, 185, 300, false));
48
+ it('Size 300x200 with coordinates', () => testAdaptSquareSizeWithIncrement(23, 300, 200, true));
49
+ it('Size 300x200 without coordinates', () => testAdaptSquareSizeWithIncrement(25, 300, 200, false));
50
+ it('Size 375x500 with coordinates', () => testAdaptSquareSizeWithIncrement(40, 375, 500, true));
51
+ it('Size 375x500 without coordinates', () => testAdaptSquareSizeWithIncrement(41, 375, 500, false));
52
+ it('Size 600x450 with coordinates', () => testAdaptSquareSizeWithIncrement(54, 600, 450, true));
53
+ it('Size 600x450 without coordinates', () => testAdaptSquareSizeWithIncrement(56, 600, 450, false));
40
54
  });
41
55
 
42
56
  describe('Adapt square-size with small-screen limits', () => {
@@ -58,18 +72,18 @@ describe('Adapt square-size with small-screen limits', () => {
58
72
 
59
73
  it('Window-limited', () => {
60
74
  window.innerWidth = 640;
61
- test.value(Chessboard.adaptSquareSize(9999, 9999, true, limits)).is(32);
75
+ testAdaptSquareSize(32, 9999, 9999, true, limits);
62
76
  });
63
77
  it('Available-space-limited 1', () => {
64
78
  window.innerWidth = 800;
65
- test.value(Chessboard.adaptSquareSize(375, 500, true, limits)).is(40);
79
+ testAdaptSquareSize(40, 375, 500, true, limits);
66
80
  });
67
81
  it('Available-space-limited 2', () => {
68
82
  window.innerWidth = 800;
69
- test.value(Chessboard.adaptSquareSize(375, 500, false, limits)).is(41);
83
+ testAdaptSquareSize(41, 375, 500, false, limits);
70
84
  });
71
85
  it('Force hidden coordinates', () => {
72
86
  window.innerWidth = 600;
73
- test.value(Chessboard.adaptSquareSize(185, 300, true, limits)).is(20);
87
+ testAdaptSquareSize(20, 185, 300, true, limits);
74
88
  });
75
89
  });