Files
shogiplayground-plugin/content.js
T
2026-09-12 16:30:35 +05:30

164 lines
4.2 KiB
JavaScript

// ## 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}!`);
});
})();