29#include "scid/core/game.h"
30#include "scid/core/nags.h"
40namespace scid::core::pgn {
43 bool symbolicNags =
false;
44 bool includeSupplementalTags =
true;
45 bool includeComments =
true;
46 bool includeVariations =
true;
47 std::optional<unsigned> lineWidth = std::nullopt;
56template <
int desired_len = 80,
char breakpoint_char =
'\0',
int hard_len = 0,
58Iter break_lines(Iter begin, Iter end) {
59 auto line_first_char = begin;
60 auto last_breakpoint = begin;
63 it = find_if(it, end, [&](
char ch) {
64 return ch ==
'\n' || ch == breakpoint_char;
70 if (std::distance(line_first_char, it) > desired_len &&
71 last_breakpoint > line_first_char) {
72 *last_breakpoint =
'\n';
73 line_first_char = last_breakpoint + 1;
79 if (hard_len != 0 && std::distance(line_first_char, it) > hard_len) {
80 line_first_char = break_lines<hard_len, ' '>(line_first_char, it);
87 line_first_char = ++it;
93 return line_first_char;
96template <
typename Iter>
97Iter break_lines(Iter begin, Iter end,
unsigned desired_len) {
98 auto line_first_char = begin;
99 auto last_breakpoint = begin;
102 it = find_if(it, end, [](
char ch) {
103 return ch ==
'\n' || ch ==
'\0';
106 if (desired_len != 0 &&
107 std::distance(line_first_char, it) > desired_len &&
108 last_breakpoint > line_first_char) {
109 *last_breakpoint =
'\n';
110 line_first_char = last_breakpoint + 1;
117 line_first_char = ++it;
119 last_breakpoint = it;
123 return line_first_char;
132template <
typename TCont>
133void escape_string(TCont& str,
typename TCont::size_type pos) {
134 auto it = str.begin() + pos;
136 it = std::find_if(it, str.end(),
137 [](
char ch) { return ch ==
'\\' || ch ==
'\"'; });
139 it = str.insert(it,
'\\') + 2;
155template <
bool unknown_to_question_mark = false,
typename TCont>
156void encode_tag_pair(std::string_view tag, std::string_view value,
159 dest.insert(dest.end(), tag.begin(), tag.end());
160 dest.push_back(
'\0');
163 if (unknown_to_question_mark && value.empty() &&
164 (tag ==
"Event" || tag ==
"Site" || tag ==
"Round" || tag ==
"White" ||
168 auto value_begin = dest.size();
169 dest.insert(dest.end(), value.begin(), value.end());
170 escape_string(dest, value_begin);
176 dest.push_back(
'\n');
183template <
int hard_len = 0,
typename TCont>
184[[nodiscard]]
bool encode_comment_rest_of_line(std::string_view comment,
186 if ((hard_len != 0 && comment.size() >= hard_len) ||
187 std::any_of(comment.begin(), comment.end(),
188 [](
char ch) { return ch ==
'\n' || ch ==
'\0'; }))
191 if (!dest.empty() && dest.back() !=
'\0' && dest.back() !=
'\n') {
192 dest.push_back(
'\0');
195 dest.insert(dest.end(), comment.begin(), comment.end());
196 dest.push_back(
'\n');
206template <
int hard_len = 0,
typename TCont>
207static void encode_comment(std::string_view comment, TCont& dest) {
208 auto is_curly = [](
char ch) {
return ch ==
'{' || ch ==
'}'; };
209 auto it_curly = std::find_if(comment.begin(), comment.end(), is_curly);
210 if (it_curly != comment.end() &&
211 encode_comment_rest_of_line<hard_len>(comment, dest))
215 dest.insert(dest.end(), comment.begin(), comment.end());
216 if (it_curly != comment.end()) {
219 auto it = dest.end() - std::distance(it_curly, comment.end());
221 auto replace_char = (*it ==
'{') ? u8
"\uFF5B" : u8
"\uFF5D";
222 static_assert(std::u8string_view(u8
"\uFF5D").size() == 3);
223 it = dest.insert(it, 2,
'\0');
224 it = std::copy_n(replace_char, 3, it);
226 it = std::find_if(it, dest.end(), is_curly);
227 }
while (it != dest.end());
230 dest.push_back(
'\0');
235enum class MovetextEntryKind {
243 MovetextEntryKind kind;
244 std::string_view san;
245 std::string_view comment;
246 std::span<const Nag> nags;
251 scid::core::sanFlagT flag) {
252 if (!move.san.empty())
255 return position.makeSan(move.spec, flag);
258template <
int hard_len = 0,
typename TCont>
259void encode_movetext_entry(MovetextEntry
const& entry,
260 std::vector<long long>& ply,
261 typename TCont::size_type& move_end,
264 switch (entry.kind) {
265 case MovetextEntryKind::InitialComment:
266 if (options.includeComments && !entry.comment.empty())
267 encode_comment<hard_len>(entry.comment, dest);
270 case MovetextEntryKind::VariationStart:
271 ply.push_back(ply.back() - 1);
273 if (options.includeComments && !entry.comment.empty())
274 encode_comment<hard_len>(entry.comment, dest);
277 case MovetextEntryKind::VariationEnd:
279 if (dest.back() ==
'\0') {
284 dest.push_back(
'\0');
287 case MovetextEntryKind::Move: {
288 auto white_to_move = (ply.back() % 2) == 0;
289 if (white_to_move || move_end != dest.size()) {
290 auto move_number = std::to_string(ply.back() / 2 + 1);
291 move_number.append(white_to_move ? 1 : 3,
'.');
292 dest.insert(dest.end(), move_number.begin(), move_number.end());
294 dest.insert(dest.end(), entry.san.begin(), entry.san.end());
295 dest.push_back(
'\0');
296 move_end = dest.size();
299 if (options.includeComments) {
300 for (
auto nag : entry.nags) {
301 auto nag_str = nagToString(nag, options.symbolicNags);
302 dest.insert(dest.end(), nag_str.begin(), nag_str.end());
303 dest.push_back(
'\0');
306 if (options.includeComments && !entry.comment.empty())
307 encode_comment<hard_len>(entry.comment, dest);
315template <
int hard_len = 0,
typename TCont>
316void encode_core_line(MoveSequence
const& line,
318 std::vector<long long>& ply,
319 typename TCont::size_type& move_end, TCont& dest,
320 EncodeOptions options = {}) {
321 for (std::size_t i = 0; i < line.moves.size(); ++i) {
322 auto const& move = line.moves[i];
323 auto position_before_move = position;
324 const auto sanFlag = i + 1 == line.moves.size()
325 ? scid::core::SAN_MATETEST
326 : scid::core::SAN_CHECKTEST;
327 const auto san = detail::san_for_move(position, move, sanFlag);
329 detail::encode_movetext_entry<hard_len>(
330 {detail::MovetextEntryKind::Move,
332 move.metadata.comment,
333 {move.metadata.nags.data(), move.metadata.nags.size()}},
334 ply, move_end, dest, options);
336 if (options.includeVariations) {
337 for (
auto const& variation : move.childVariations) {
338 detail::encode_movetext_entry<hard_len>(
339 {detail::MovetextEntryKind::VariationStart,
341 variation.initialComment,
343 ply, move_end, dest, options);
344 encode_core_line<hard_len>(variation.line, position_before_move,
345 ply, move_end, dest, options);
346 detail::encode_movetext_entry<hard_len>(
347 {detail::MovetextEntryKind::VariationEnd, {}, {}, {}},
348 ply, move_end, dest, options);
352 (void)position.applyMove(move.spec);
356template <
int hard_len = 0,
typename TCont>
357void encode_movetext(Game
const& game, TCont& dest,
358 EncodeOptions options = {}) {
359 std::vector<long long> ply = {game.initialPlyCounter()};
360 auto move_end = dest.size();
361 dest.push_back(
'\n');
363 if (options.includeComments && !game.initialComment().empty()) {
364 detail::encode_movetext_entry<hard_len>(
365 {detail::MovetextEntryKind::InitialComment,
367 game.initialComment(),
369 ply, move_end, dest, options);
372 auto position = game.startPosition() ? *game.startPosition()
373 : scid::core::Position::getStdStart();
374 encode_core_line<hard_len>(game.movetext().mainline, position, ply,
375 move_end, dest, options);
377 if (dest.back() ==
'\0')
381template <
typename TCont>
382void encode_core_tag_pairs(Game
const& game, TCont& dest,
383 EncodeOptions options = {}) {
385 encode_tag_pair(
"Event", game.event(), dest);
386 encode_tag_pair(
"Site", game.site(), dest);
387 scid::core::date_DecodeToString(game.date(), str_buf);
388 encode_tag_pair(
"Date", str_buf, dest);
389 encode_tag_pair(
"Round", game.round(), dest);
390 encode_tag_pair(
"White", game.white().name, dest);
391 encode_tag_pair(
"Black", game.black().name, dest);
392 encode_tag_pair(
"Result", game.resultString(), dest);
394 if (options.includeSupplementalTags) {
395 if (
auto rating = game.white().rating.value) {
396 std::string tag =
"White";
397 tag.append(scid::core::ratingTypeNames[game.white().rating.type]);
398 encode_tag_pair(tag, std::to_string(rating), dest);
400 if (
auto rating = game.black().rating.value) {
401 std::string tag =
"Black";
402 tag.append(scid::core::ratingTypeNames[game.black().rating.type]);
403 encode_tag_pair(tag, std::to_string(rating), dest);
405 if (!game.eco().empty())
406 encode_tag_pair(
"ECO", game.eco(), dest);
407 if (game.eventDate() != scid::core::ZERO_DATE) {
408 scid::core::date_DecodeToString(game.eventDate(), str_buf);
409 encode_tag_pair(
"EventDate", str_buf, dest);
411 for (
auto const& tag : game.extraTags())
412 encode_tag_pair(tag.first, tag.second, dest);
414 if (game.hasNonStandardStart(str_buf,
sizeof(str_buf)))
415 encode_tag_pair(
"FEN", str_buf, dest);
418template <
int hard_len = 0,
typename TCont>
419void encode_game(Game
const& game, TCont& dest, EncodeOptions options = {}) {
420 encode_core_tag_pairs(game, dest, options);
421 encode_movetext<hard_len>(game, dest, options);
423 auto result = game.resultString();
424 dest.insert(dest.end(), result.begin(), result.end());
425 dest.push_back(
'\n');
432template <
int desired_len = 80,
typename TGame,
typename TCont>
433void encode(TGame
const& game, TCont& dest, EncodeOptions options = {}) {
434 auto begin = dest.size();
435 encode_game(game, dest, options);
436 if (options.lineWidth) {
437 break_lines(dest.begin() + begin, dest.end(), *options.lineWidth);
439 break_lines<desired_len>(dest.begin() + begin, dest.end());