// ## kif.js // KIF is the most popular file format used to represent full shogi games, // studies of games, tsume problems and other types of puzzles. // For more information, see https://lishogi.org/page/kif and // https://deepwiki.com/gunyarakun/python-shogi/3.1-kif-format const pieceMap = { P: "歩", L: "香", N: "桂", S: "銀", G: "金", B: "角", R: "飛", K: "玉", "+P": "と", "+L": "成香", "+N": "成桂", "+S": "成銀", "+B": "馬", "+R": "竜" }; const japaneseNumberMap = { 1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "七", 8: "八", 9: "九" }; function kifCoord(square) { const file = square[0]; const rank = square[1]; return `${file}${japaneseNumberMap[rank]}`; } // A struct to hold the source or destination square of a move // file and rank are numbers from 1-9 const Square = (file, rank) => ({ file, rank }); function parseSquare(square) { // square is a string like "7g" // first character is the file (1-9), second character is the rank (a-i) const file = parseInt(square[0], 10); const rank = "abcdefghi".indexOf(square[1]) + 1; return Square(file, rank); } function toKIF(line, prevDest) { // move number const move = line.replace(/^\d+:\s*/, "").trim(); // gote/sente player indicator const player = move[0] === "☗" ? "▲" : "△"; var body = move.slice(1); // piece if (move[0] === "+") { // promoted piece // first two characters are the piece, then the destination const piece = body.slice(0, 2); body = body.slice(2); } else { // first character is the piece, then the destination const piece = body[0]; body = body.slice(1); } // source square (optional) if (body[0] >= "1" && body[0] <= "9") { const sourceSquare = body.slice(0, 2); body = body.slice(2); } // move type moveType = body[0]; // either '-' for a normal move, 'x' for a capture, or '*' for a drop body = body.slice(1); // destination square destSquare = body.slice(0, 2); body = body.slice(2); if (body.length > 0) { if (body[0] === "+" || body[0] === "=") { const promoFlag = body[0]; body = body.slice(1); } } // to KIF format var kifMove = `${player}${kifCoord(destSquare)}${pieceMap[piece]}${moveType === "*" ? "打" : ""}`; if (promoFlag) { kifMove += promoFlag === "+" ? "成" : "不成"; } return kifMove; } // ## content.js // This module provides functions to convert the shogiplayground // (https://play.mogproject.com/) format to KIF. (() => { const sidebar = document.querySelector("div.sidebar-left-content"); if (!sidebar) { alert("Sidebar not found!"); return; } const branchSelect = sidebar.querySelector("select"); const copyButton = sidebar.querySelector("button"); if (!branchSelect || !copyButton) { alert("Branch selector or copy button not found!"); return; } const results = []; const collectMoves = async () => { for (let i = 0; i < branchSelect.options.length; i++) { branchSelect.selectedIndex = i; branchSelect.dispatchEvent(new Event("change")); await new Promise(r => setTimeout(r, 200)); const text = copyButton.getAttribute("data-clipboard-text") || ""; let prevDest = null; const lines = text.split("\n").map(l => { const kif = toKIF(l, prevDest); if (kif) { const match = kif.match(/^[▲△](同|\d[一二三四五六七八九])/); if (match) prevDest = match[1] === "同" ? prevDest : match[0].slice(1); } return kif; }).filter(Boolean); const label = branchSelect.options[i].textContent.trim(); results.push(`${label}:\n${lines.join("\n")}`); } }; const default_filename = "shogiplayground-plugin.kif"; collectMoves().then(() => { const finalText = results.join("\n\n"); const blob = new Blob([finalText], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = default_filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); alert(`All branches exported to ${default_filename}!`); }); })();