kokopu-react 1.5.5 → 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,11 @@
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
+
4
9
  1.5.5 (May 18, 2022)
5
10
  --------------------
6
11
  * Upgrade dependencies.
@@ -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;
@@ -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...).
@@ -254,7 +254,8 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
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';
@@ -564,40 +565,6 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
564
565
  this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
565
566
  }
566
567
  }
567
- /**
568
- * Return the square at the given location.
569
- */
570
-
571
- }, {
572
- key: "getNotationTextBuilder",
573
- value: function getNotationTextBuilder() {
574
- var pieceSymbols = this.props.pieceSymbols;
575
-
576
- if (pieceSymbols === 'localized') {
577
- var mapping = _i18n["default"].PIECE_SYMBOLS;
578
- return function (notation) {
579
- return notation.replace(/[KQRBNP]/g, function (match) {
580
- return mapping[match];
581
- });
582
- };
583
- } else if (pieceSymbols === 'figurines') {
584
- return function (notation) {
585
- return figurineNotation(notation, 'alpha');
586
- };
587
- } else if (pieceSymbols !== 'native' && pieceSymbols && ['K', 'Q', 'R', 'B', 'N', 'P'].every(function (p) {
588
- return typeof pieceSymbols[p] === 'string';
589
- })) {
590
- return function (notation) {
591
- return notation.replace(/[KQRBNP]/g, function (match) {
592
- return pieceSymbols[match];
593
- });
594
- };
595
- } else {
596
- return function (notation) {
597
- return notation;
598
- };
599
- }
600
- }
601
568
  /**
602
569
  * Set the focus to the current component.
603
570
  *
@@ -846,58 +813,50 @@ function formatResult(result) {
846
813
  }
847
814
  }
848
815
 
849
- 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
+
850
846
  return new _HtmlSanitizer["default"]({
851
- allowedTags: ['a', 'span', 'b', 'strong', 'i', 'em', 'mark', 'small', 'del', 'ins', 'sub', 'sup'],
852
- allowedAttributes: {
853
- '*': ['class', 'id', 'title'],
854
- 'a': ['href', 'target']
855
- }
847
+ allowedTags: allowedTags,
848
+ allowedAttributes: allowedAttributes
856
849
  });
857
850
  }
858
851
 
859
852
  function sanitizeHtml(text, sanitizer) {
860
853
  if (!sanitizer) {
861
- sanitizer = createSanitizer();
854
+ sanitizer = createSanitizer(false);
862
855
  }
863
856
 
864
857
  var result = sanitizer.parse(text);
865
858
  return result !== null && result !== void 0 ? result : text;
866
859
  }
867
- /**
868
- * Decompose the given string into piece symbol characters and sections of non piece symbol characters, and transform the piece symbols into
869
- * React objects represented with the given chess font.
870
- */
871
-
872
-
873
- function figurineNotation(text, fontName) {
874
- var result = [];
875
- var beginOfText = 0;
876
- var pieceSymbolIndex = 0;
877
-
878
- for (var pos = 0; pos < text.length; ++pos) {
879
- var currentChar = text.charAt(pos);
880
-
881
- if (currentChar === 'K' || currentChar === 'Q' || currentChar === 'R' || currentChar === 'B' || currentChar === 'N' || currentChar === 'P') {
882
- if (pos > beginOfText) {
883
- result.push(text.substring(beginOfText, pos));
884
- }
885
-
886
- beginOfText = pos + 1;
887
- var key = 'symbol-' + pieceSymbolIndex++;
888
- result.push( /*#__PURE__*/_react["default"].createElement("span", {
889
- className: 'kokopu-font-' + fontName,
890
- key: key
891
- }, currentChar));
892
- }
893
- }
894
-
895
- if (beginOfText < text.length) {
896
- result.push(text.substring(beginOfText));
897
- }
898
-
899
- return result;
900
- }
901
860
  /**
902
861
  * Try to interpret the given object as a chess game.
903
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"));
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "kokopu-react",
3
- "version": "1.5.5",
3
+ "version": "1.6.0",
4
4
  "description": "A React-based library to create and display chessboard and chess-related components.",
5
5
  "keywords": [
6
6
  "chess",
@@ -38,19 +38,19 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@babel/cli": "^7.17.10",
41
- "@babel/core": "^7.17.12",
42
- "@babel/preset-env": "^7.17.12",
41
+ "@babel/core": "^7.18.2",
42
+ "@babel/preset-env": "^7.18.2",
43
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.8.0",
46
+ "@mui/material": "^5.8.2",
47
47
  "babel-loader": "^8.2.5",
48
48
  "babel-plugin-istanbul": "^6.1.1",
49
49
  "copy-webpack-plugin": "^11.0.0",
50
50
  "coveralls": "^3.1.1",
51
51
  "css-loader": "^6.7.1",
52
- "eslint": "^8.15.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
56
  "mocha": "^10.0.0",
@@ -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/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
 
@@ -144,7 +144,8 @@ export default class Movetext extends React.Component {
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>;
@@ -368,26 +369,6 @@ export default class Movetext extends React.Component {
368
369
  }
369
370
  }
370
371
 
371
- /**
372
- * Return the square at the given location.
373
- */
374
- getNotationTextBuilder() {
375
- let pieceSymbols = this.props.pieceSymbols;
376
- if (pieceSymbols === 'localized') {
377
- let mapping = i18n.PIECE_SYMBOLS;
378
- return notation => notation.replace(/[KQRBNP]/g, match => mapping[match]);
379
- }
380
- else if (pieceSymbols === 'figurines') {
381
- return notation => figurineNotation(notation, 'alpha');
382
- }
383
- else if (pieceSymbols !== 'native' && pieceSymbols && ['K', 'Q', 'R', 'B', 'N', 'P'].every(p => typeof pieceSymbols[p] === 'string')) {
384
- return notation => notation.replace(/[KQRBNP]/g, match => pieceSymbols[match]);
385
- }
386
- else {
387
- return notation => notation;
388
- }
389
- }
390
-
391
372
  /**
392
373
  * Set the focus to the current component.
393
374
  *
@@ -615,52 +596,49 @@ function formatResult(result) {
615
596
  }
616
597
 
617
598
 
618
- function createSanitizer() {
619
- return new HtmlSanitizer({
620
- allowedTags: [ 'a', 'span', 'b', 'strong', 'i', 'em', 'mark', 'small', 'del', 'ins', 'sub', 'sup' ],
621
- allowedAttributes: {
622
- '*': [ 'class', 'id', 'title' ],
623
- 'a': [ 'href', 'target' ]
624
- }
625
- });
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 });
626
630
  }
627
631
 
628
632
 
629
633
  function sanitizeHtml(text, sanitizer) {
630
634
  if (!sanitizer) {
631
- sanitizer = createSanitizer();
635
+ sanitizer = createSanitizer(false);
632
636
  }
633
637
  let result = sanitizer.parse(text);
634
638
  return result ?? text;
635
639
  }
636
640
 
637
641
 
638
- /**
639
- * Decompose the given string into piece symbol characters and sections of non piece symbol characters, and transform the piece symbols into
640
- * React objects represented with the given chess font.
641
- */
642
- function figurineNotation(text, fontName) {
643
- let result = [];
644
- let beginOfText = 0;
645
- let pieceSymbolIndex = 0;
646
- for (let pos = 0; pos < text.length; ++pos) {
647
- let currentChar = text.charAt(pos);
648
- if (currentChar === 'K' || currentChar === 'Q' || currentChar === 'R' || currentChar === 'B' || currentChar === 'N' || currentChar === 'P') {
649
- if (pos > beginOfText) {
650
- result.push(text.substring(beginOfText, pos));
651
- }
652
- beginOfText = pos + 1;
653
- let key = 'symbol-' + (pieceSymbolIndex++);
654
- result.push(<span className={'kokopu-font-' + fontName} key={key}>{currentChar}</span>);
655
- }
656
- }
657
- if (beginOfText < text.length) {
658
- result.push(text.substring(beginOfText));
659
- }
660
- return result;
661
- }
662
-
663
-
664
642
  /**
665
643
  * Try to interpret the given object as a chess game.
666
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';