Re: 將製作ejcees(中國象棋打譜程式)
发表于 : 2026年 3月 28日 09:37
實現導入東萍象棋棋譜,可包含分支:
https://ejsoon.vip/wp-content/uploads/2 ... 99771.html
https://ejsoon.vip/wp-content/uploads/2 ... 99771.html
手捧一份淨土,劃分文學、外語、音樂、茶座、IT等圈子,寓意為在一個個圈子裡與你相遇,在這里圈圈和朋友們討論酷的東西
https://quanquan.space/
代码: 全选
改進以下代碼:
一,當path的根節點存在path.c且不為空時,在「fen + '\n'」的後面加上「path.c + \n」。
二,當任何節點存在path.c且不為空時,在它下方加「\n + path.c + \n」。如果輪到紅方,則加完後,接下來黑方會加入 「...」省略號。
例如之前是「1. 炮二平五 馬8進7」,現在就變為「
1. 炮二平五
對於炮二平五的評論。
1. ... 馬8進7
」
給出完整的generateCnTextWidthComment函式,所有代碼和注釋都要使用英文。
function generateCnTextWidthComment() {
let path = getGamePath();
let fen = path.fen;
let text = fen + '\n';
let isRedTurn = fen.split(' ')[1] === 'w';
let moveIdx = 0;
let moveNum = 1;
while (path.v.length) {
let line = `${moveNum}. `;
if (!isRedTurn && moveIdx === 0) {
path = path.v[0];
let blkMove = isEnglish ? path.move : NotationConverter.toChinese(path.move);
line += `... ${blkMove}`;
moveIdx++;
} else {
path = path.v[0];
let redMove = isEnglish ? path.move : NotationConverter.toChinese(path.move);
line += `${redMove}`;
moveIdx++;
if (path.v.length) {
path = path.v[0];
let blkMove = isEnglish ? path.move : NotationConverter.toChinese(path.move);
line += ` ${blkMove}`;
moveIdx++;
}
}
text += line + '\n';
moveNum++;
}
return text.trim();
}
代码: 全选
/**
* Generates game notation text including FEN, moves, and comments.
* Handles automatic ellipsis (...) insertion when a comment interrupts a turn.
*/
function generateCnTextWidthComment() {
const pathRoot = getGamePath();
const fen = pathRoot.fen;
let text = fen + '\n';
// Requirement 1: Add root comment if it exists and is not empty
if (pathRoot.c && pathRoot.c.trim() !== '') {
text += pathRoot.c.trim() + '\n';
}
let isRedTurn = fen.split(' ')[1] === 'w';
let moveNum = 1; // Standard starting move number
let currentNode = pathRoot;
// Tracks if the previous move was interrupted by a comment
// or if the game started on a Black move.
let needsEllipsis = !isRedTurn;
while (currentNode.v && currentNode.v.length > 0) {
// Move to the next node in the main variation
currentNode = currentNode.v[0];
// Convert move notation based on language setting
const moveStr = typeof isEnglish !== 'undefined' && isEnglish
? currentNode.move
: NotationConverter.toChinese(currentNode.move);
if (isRedTurn) {
// RED TURN PROCESSING
text += `${moveNum}. ${moveStr}`;
// Requirement 2: Check for comment after Red move
if (currentNode.c && currentNode.c.trim() !== '') {
text += `\n${currentNode.c.trim()}\n`;
needsEllipsis = true; // Black will need "..." on the next line
} else {
text += " "; // Add space to separate from potential Black move
needsEllipsis = false;
}
isRedTurn = false; // Toggle turn to Black
} else {
// BLACK TURN PROCESSING
if (needsEllipsis) {
// If interrupted by Red's comment or starting mid-turn
text += `${moveNum}. ... ${moveStr}\n`;
} else {
// Standard flow: Black follows Red on the same line
text += `${moveStr}\n`;
}
// Requirement 2: Check for comment after Black move
if (currentNode.c && currentNode.c.trim() !== '') {
text += `${currentNode.c.trim()}\n`;
}
moveNum++; // Increment move number after Black finishes
isRedTurn = true; // Toggle turn to Red
needsEllipsis = false; // Reset ellipsis flag
}
}
return text.trim();
}代码: 全选
改進renderExportTextUI函式:
function importExportedText(text) {
text = text.trim();
if (!text) {
renderRecordUI();
renderNoteUI();
updateToolHighlights();
return;
}
// Normalize Chinese notations
text = text.replace(/车/g, '車').replace(/马/g, '馬').replace(/帅/g, '帥').replace(/将/g, '將').replace(/后/g, '後').replace(/进/g, '進');
text = text.replace(/ r(\n|\s|$)/g, ' w$1');
// Handle URL format directly if pasted
if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
let hashPart = text.includes('#') ? text.split('#')[1] : text;
if (hashPart) {
try {
let parsedTree = parseUrlHash(hashPart);
if (parsedTree && parsedTree.fen) {
historyFEN = parsedTree;
initBranch();
currentStepIndex = 0;
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length >= 4) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
}
} catch (e) {
console.log("Failed to parse as URL parameter, falling back to text notation.", e);
}
}
}
// Handle Direct JSON structure
if (text.startsWith('{')) {
try {
const data = JSON.parse(text);
function expand(node, parentFen) {
let dc, simfen, newNode;
if (node.m) {
dc = deriveCoordsFromMove(parentFen, node.m);
simfen = simulateMove(parentFen, dc);
newNode = {
fen: simfen,
move: node.m,
lastMove: dc,
c: node.c || "",
v: []
};
} else {
newNode = {
fen: parentFen,
c: node.c || "",
v: []
};
}
if (node.v) {
newNode.v = node.v.map(child => expand(child, newNode.fen));
}
return newNode;
}
historyFEN = expand(data, data.fen);
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length === 6) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
initBranch(); // initBranch correctly builds currentBranch based on forks
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
} catch (e) {
console.log("JSON parse failed, processing as text notation.");
}
}
// 1. Locate explicitly provided FEN if present
let importedFen = null;
const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
const fenMatch = text.match(fenRegex);
let movesStr = text;
if (fenMatch) {
importedFen = fenMatch[0];
// Strip the FEN string declaration so it isn't parsed as moves later
const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
movesStr = movesStr.replace(cleanRegex, '').trim();
}
// 2. Identify the Attach Point (Parent Node)
let attachIndex = currentStepIndex;
let attachNode = null;
if (importedFen) {
// If FEN exists, trace backwards to find a matching state
for (let i = currentStepIndex; i >= 0; i--) {
const node = getNodeAtStep(i);
if (node && node.fen === importedFen) {
attachIndex = i;
attachNode = node;
break;
}
}
// No match down to root -> Overwrite everything
if (!attachNode) {
attachIndex = 0;
attachNode = {
fen: importedFen,
move: null,
lastMove: null,
c: "",
v: []
};
historyFEN = attachNode;
currentBranch = [];
}
} else {
// No FEN -> Default attach point is the current active step
attachNode = getNodeAtStep(attachIndex);
importedFen = attachNode.fen;
}
// 3. Process move sequence text
// Rule 1: Split text by \n and trim to form an array.
let lines = movesStr.split('\n').map(l => l.trim());
// Check if any line matches the "^\d+\." pattern to determine the parsing mode.
let isComplexFormat = lines.some(l => /^\d+\./.test(l));
let currentFen = importedFen;
let vMap = buildVirtualMap(currentFen);
let currNode = attachNode;
let currentCommentNode = attachNode;
// Calculate how many forks exist from root up to attachIndex
let forkCount = 0;
let tempNode = historyFEN;
for (let i = 0; i < attachIndex; i++) {
if (tempNode.v && tempNode.v.length > 1) {
forkCount++;
}
const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
tempNode = tempNode.v[choice];
}
// Truncate currentBranch to discard choices beyond the attach point
currentBranch = currentBranch.slice(0, forkCount);
let hasError = false;
if (isComplexFormat) {
let branchMap = {};
let seenCircles = new Set();
let lastMoveParent = attachNode;
let lastMoveParentFen = importedFen;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (!line) continue;
if (!/^\d+\./.test(line)) {
// Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
let isBranchDecl = false;
// Rule 4 & 5: Find the branch insertion point (second occurrence)
for (let c of circles) {
if (seenCircles.has(c) && branchMap[c]) {
currNode = branchMap[c].parentNode;
currentFen = branchMap[c].fen;
vMap = buildVirtualMap(currentFen);
currentCommentNode = currNode;
isBranchDecl = true;
delete branchMap[c]; // Only care about the first two occurrences
break; // Switch context once per line
}
}
// Accumulate comments
if (!isBranchDecl) {
if (!currentCommentNode.c) currentCommentNode.c = "";
currentCommentNode.c += (currentCommentNode.c ? "\n" : "") + line;
}
} else {
// Rule 1 & 2: Line starting with "^\d+\.", strip the number, then split by spaces
let cleanLine = line.replace(/^\d+\./, '').trim();
let tokens = cleanLine.split(/\s+/);
for (let token of tokens) {
// Ignore standalone ellipses
if (token === '...' || token === '…') continue;
let circles = token.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
// Strip circled numbers and ellipses to get the pure move string
let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]/g, '');
if (moveStr.length >= 4) {
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
let startX = moveStr.charCodeAt(0) - 97;
let startY = 9 - parseInt(moveStr.charAt(1), 10);
let endX = moveStr.charCodeAt(2) - 97;
let endY = 9 - parseInt(moveStr.charAt(3), 10);
dc = {
startX,
startY,
endX,
endY
};
let pId = vMap.get(`${startX},${startY}`);
if (!pId) {
hasError = true;
continue;
}
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);
moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
// If coordinates cannot be derived, treat the token as a comment
// for the current node (which represents the previous step)
if (!currentCommentNode.c) {
currentCommentNode.c = "";
}
// Append the token, separating with a space if a comment already exists
currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
continue; // Skip node creation and continue parsing
}
// Rule 4: The variation is attached to the parent of the move
lastMoveParent = currNode;
lastMoveParentFen = currentFen;
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = {
fen: nextFen,
move: moveEn,
lastMove: dc,
c: "",
v: []
};
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
// If this creates a fork on the main path, update currentBranch
if (currNode.v.length > 1 && currNode === attachNode) {
currentBranch[forkCount] = childIdx;
forkCount++;
attachNode = currNode.v[childIdx]; // Follow the first parsed line as the main path
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
currentCommentNode = currNode;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
// Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
for (let c of circles) {
if (!seenCircles.has(c)) {
seenCircles.add(c);
// Map the first occurrence to the parent node of the currently parsed move
branchMap[c] = {
parentNode: lastMoveParent,
fen: lastMoveParentFen
};
}
}
}
}
}
} else {
// Fallback for simple flat formats (no "^\d+\." format detected)
let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
let tokensStartIdx = 0;
for (let i = 0; i < tokens.length; i++) {
if (/^1\./.test(tokens[i])) {
tokensStartIdx = i;
break;
}
}
for (let i = tokensStartIdx; i < tokens.length; i++) {
let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
if (/^[a-i][0-9][a-i][0-9]$/.test(token)) {
let startX = token.charCodeAt(0) - 97;
let startY = 9 - parseInt(token.charAt(1), 10);
let endX = token.charCodeAt(2) - 97;
let endY = 9 - parseInt(token.charAt(3), 10);
dc = {
startX,
startY,
endX,
endY
};
let pId = vMap.get(`${startX},${startY}`);
if (!pId) {
hasError = true;
break;
}
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);
moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
if (!dc) {
hasError = true;
break;
}
}
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = {
fen: nextFen,
move: moveEn,
lastMove: dc,
c: "",
v: []
};
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
if (currNode.v.length > 1) {
currentBranch[forkCount] = childIdx;
forkCount++;
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
}
if (hasError) {
alert("Parsed partially due to an invalid move notation.");
}
// Fill default choices (0) for any remaining branches down the tree
let followNode = attachNode;
let followIdx = 0;
while (followNode && followIdx < currentBranch.length) {
if (followNode.v && followNode.v.length > 1) {
followNode = followNode.v[currentBranch[followIdx]];
} else if (followNode.v && followNode.v.length === 1) {
followNode = followNode.v[0];
} else {
break;
}
followIdx++;
}
while (followNode && followNode.v && followNode.v.length > 0) {
if (followNode.v.length > 1) {
currentBranch.push(0);
}
followNode = followNode.v[0];
}
currentStepIndex = attachIndex;
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
updateToolHighlights();
}
在這一段:「
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
// If coordinates cannot be derived, treat the token as a comment
// for the current node (which represents the previous step)
if (!currentCommentNode.c) {
currentCommentNode.c = "";
}
// Append the token, separating with a space if a comment already exists
currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
continue; // Skip node creation and continue parsing
}
」
它的意義為,如果用deriveCoordsFromMove轉換失敗,則token將成為上一步的評論。
現在加上一個判斷,如果是首個token就轉換失敗,則把局面改為「新建局面」導入,如果在新建局面下首個token仍然轉換失敗,則取消導入,恢復為導入之前的狀態。
新建局面主要影響的是這三個變量:
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
給出完整的importExportedText函式,所有代碼和注釋都要使用英文。代码: 全选
function importExportedText(text) {
text = text.trim();
if (!text) {
renderRecordUI();
renderNoteUI();
updateToolHighlights();
return;
}
// Backup current state in case the import fails completely on the first move
const backupHistoryFEN = JSON.parse(JSON.stringify(historyFEN));
const backupBranch = [...currentBranch];
const backupStepIndex = currentStepIndex;
// Normalize Chinese notations
text = text.replace(/车/g, '車').replace(/马/g, '馬').replace(/帅/g, '帥').replace(/将/g, '將').replace(/后/g, '後').replace(/进/g, '進');
text = text.replace(/ r(\n|\s|$)/g, ' w$1');
// Handle URL format directly if pasted
if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
let hashPart = text.includes('#') ? text.split('#')[1] : text;
if (hashPart) {
try {
let parsedTree = parseUrlHash(hashPart);
if (parsedTree && parsedTree.fen) {
historyFEN = parsedTree;
initBranch();
currentStepIndex = 0;
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length >= 4) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
}
} catch (e) {
console.log("Failed to parse as URL parameter, falling back to text notation.", e);
}
}
}
// Handle Direct JSON structure
if (text.startsWith('{')) {
try {
const data = JSON.parse(text);
function expand(node, parentFen) {
let dc, simfen, newNode;
if (node.m) {
dc = deriveCoordsFromMove(parentFen, node.m);
simfen = simulateMove(parentFen, dc);
newNode = {
fen: simfen,
move: node.m,
lastMove: dc,
c: node.c || "",
v: []
};
} else {
newNode = {
fen: parentFen,
c: node.c || "",
v: []
};
}
if (node.v) {
newNode.v = node.v.map(child => expand(child, newNode.fen));
}
return newNode;
}
historyFEN = expand(data, data.fen);
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length === 6) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
initBranch(); // initBranch correctly builds currentBranch based on forks
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
} catch (e) {
console.log("JSON parse failed, processing as text notation.");
}
}
// 1. Locate explicitly provided FEN if present
let importedFen = null;
const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
const fenMatch = text.match(fenRegex);
let movesStr = text;
if (fenMatch) {
importedFen = fenMatch[0];
// Strip the FEN string declaration so it isn't parsed as moves later
const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
movesStr = movesStr.replace(cleanRegex, '').trim();
}
// 2. Identify the Attach Point (Parent Node)
let attachIndex = currentStepIndex;
let attachNode = null;
if (importedFen) {
// If FEN exists, trace backwards to find a matching state
for (let i = currentStepIndex; i >= 0; i--) {
const node = getNodeAtStep(i);
if (node && node.fen === importedFen) {
attachIndex = i;
attachNode = node;
break;
}
}
// No match down to root -> Overwrite everything
if (!attachNode) {
attachIndex = 0;
attachNode = {
fen: importedFen,
move: null,
lastMove: null,
c: "",
v: []
};
historyFEN = attachNode;
currentBranch = [];
}
} else {
// No FEN -> Default attach point is the current active step
attachNode = getNodeAtStep(attachIndex);
importedFen = attachNode.fen;
}
// 3. Process move sequence text
// Rule 1: Split text by \n and trim to form an array.
let lines = movesStr.split('\n').map(l => l.trim());
// Check if any line matches the "^\d+\." pattern to determine the parsing mode.
let isComplexFormat = lines.some(l => /^\d+\./.test(l));
let currentFen = importedFen;
let vMap = buildVirtualMap(currentFen);
let currNode = attachNode;
let currentCommentNode = attachNode;
// Calculate how many forks exist from root up to attachIndex
let forkCount = 0;
let tempNode = historyFEN;
for (let i = 0; i < attachIndex; i++) {
if (tempNode.v && tempNode.v.length > 1) {
forkCount++;
}
const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
tempNode = tempNode.v[choice];
}
// Truncate currentBranch to discard choices beyond the attach point
currentBranch = currentBranch.slice(0, forkCount);
let hasError = false;
let isFirstMoveAttempt = true; // Flag to track the first move parsing attempt
if (isComplexFormat) {
let branchMap = {};
let seenCircles = new Set();
let lastMoveParent = attachNode;
let lastMoveParentFen = importedFen;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (!line) continue;
if (!/^\d+\./.test(line)) {
// Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
let isBranchDecl = false;
// Rule 4 & 5: Find the branch insertion point (second occurrence)
for (let c of circles) {
if (seenCircles.has(c) && branchMap[c]) {
currNode = branchMap[c].parentNode;
currentFen = branchMap[c].fen;
vMap = buildVirtualMap(currentFen);
currentCommentNode = currNode;
isBranchDecl = true;
delete branchMap[c]; // Only care about the first two occurrences
break; // Switch context once per line
}
}
// Accumulate comments
if (!isBranchDecl) {
if (!currentCommentNode.c) currentCommentNode.c = "";
currentCommentNode.c += (currentCommentNode.c ? "\n" : "") + line;
}
} else {
// Rule 1 & 2: Line starting with "^\d+\.", strip the number, then split by spaces
let cleanLine = line.replace(/^\d+\./, '').trim();
let tokens = cleanLine.split(/\s+/);
for (let token of tokens) {
// Ignore standalone ellipses
if (token === '...' || token === '…') continue;
let circles = token.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
// Strip circled numbers and ellipses to get the pure move string
let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]/g, '');
if (moveStr.length >= 4) {
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);
if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
let startX = moveStr.charCodeAt(0) - 97;
let startY = 9 - parseInt(moveStr.charAt(1), 10);
let endX = moveStr.charCodeAt(2) - 97;
let endY = 9 - parseInt(moveStr.charAt(3), 10);
dc = { startX, startY, endX, endY };
let pId = vMap.get(`${startX},${startY}`);
if (!pId) {
hasError = true;
continue;
}
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
if (isFirstMoveAttempt) {
// If the very first move fails, try it with INITIAL_FEN
let testIsRed = INITIAL_FEN.includes(' w');
let testMoveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, testIsRed);
let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);
if (!testDc) {
// Fails even on INITIAL_FEN, abort import completely
historyFEN = backupHistoryFEN;
currentBranch = backupBranch;
currentStepIndex = backupStepIndex;
console.warn("Import aborted: Failed to parse the first move token.");
return;
} else {
// Success with INITIAL_FEN. Reset the game to "New Game" state.
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
attachIndex = 0;
// Re-initialize parsing traversal variables
attachNode = historyFEN;
currNode = historyFEN;
currentCommentNode = historyFEN;
currentFen = INITIAL_FEN;
vMap = buildVirtualMap(currentFen);
forkCount = 0;
lastMoveParent = historyFEN;
lastMoveParentFen = INITIAL_FEN;
// Apply the successful test data
dc = testDc;
isRed = testIsRed;
moveEn = testMoveEn;
}
} else {
// If coordinates cannot be derived and it's not the first move, treat as a comment
if (!currentCommentNode.c) {
currentCommentNode.c = "";
}
currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
continue; // Skip node creation and continue parsing
}
}
// Flag that we have successfully processed at least one valid move
isFirstMoveAttempt = false;
// Rule 4: The variation is attached to the parent of the move
lastMoveParent = currNode;
lastMoveParentFen = currentFen;
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = {
fen: nextFen,
move: moveEn,
lastMove: dc,
c: "",
v: []
};
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
// If this creates a fork on the main path, update currentBranch
if (currNode.v.length > 1 && currNode === attachNode) {
currentBranch[forkCount] = childIdx;
forkCount++;
attachNode = currNode.v[childIdx]; // Follow the first parsed line as the main path
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
currentCommentNode = currNode;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
// Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
for (let c of circles) {
if (!seenCircles.has(c)) {
seenCircles.add(c);
// Map the first occurrence to the parent node of the currently parsed move
branchMap[c] = {
parentNode: lastMoveParent,
fen: lastMoveParentFen
};
}
}
}
}
}
} else {
// Fallback for simple flat formats (no "^\d+\." format detected)
let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
let tokensStartIdx = 0;
for (let i = 0; i < tokens.length; i++) {
if (/^1\./.test(tokens[i])) {
tokensStartIdx = i;
break;
}
}
for (let i = tokensStartIdx; i < tokens.length; i++) {
let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);
if (/^[a-i][0-9][a-i][0-9]$/.test(token)) {
let startX = token.charCodeAt(0) - 97;
let startY = 9 - parseInt(token.charAt(1), 10);
let endX = token.charCodeAt(2) - 97;
let endY = 9 - parseInt(token.charAt(3), 10);
dc = { startX, startY, endX, endY };
let pId = vMap.get(`${startX},${startY}`);
if (!pId) {
hasError = true;
break;
}
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
if (!dc) {
if (isFirstMoveAttempt) {
// Try with INITIAL_FEN
let testIsRed = INITIAL_FEN.includes(' w');
let testMoveEn = isEnglish ? token : NotationConverter.toEnglish(token, testIsRed);
let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);
if (!testDc) {
// Fails completely, restore state
historyFEN = backupHistoryFEN;
currentBranch = backupBranch;
currentStepIndex = backupStepIndex;
console.warn("Import aborted: Failed to parse the first move token in simple format.");
return;
} else {
// Reset state to New Game
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
attachNode = historyFEN;
currNode = historyFEN;
currentFen = INITIAL_FEN;
vMap = buildVirtualMap(currentFen);
forkCount = 0;
dc = testDc;
isRed = testIsRed;
moveEn = testMoveEn;
}
} else {
hasError = true;
break;
}
}
}
isFirstMoveAttempt = false;
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = {
fen: nextFen,
move: moveEn,
lastMove: dc,
c: "",
v: []
};
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
if (currNode.v.length > 1) {
currentBranch[forkCount] = childIdx;
forkCount++;
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
}
if (hasError) {
alert("Parsed partially due to an invalid move notation.");
}
// Fill default choices (0) for any remaining branches down the tree
let followNode = attachNode;
let followIdx = 0;
while (followNode && followIdx < currentBranch.length) {
if (followNode.v && followNode.v.length > 1) {
followNode = followNode.v[currentBranch[followIdx]];
} else if (followNode.v && followNode.v.length === 1) {
followNode = followNode.v[0];
} else {
break;
}
followIdx++;
}
while (followNode && followNode.v && followNode.v.length > 0) {
if (followNode.v.length > 1) {
currentBranch.push(0);
}
followNode = followNode.v[0];
}
currentStepIndex = attachIndex;
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
updateToolHighlights();
}代码: 全选
對以下的代碼作改進:
function importExportedText(text) {
text = text.trim();
if (!text) {
renderRecordUI();
renderNoteUI();
updateToolHighlights();
return;
}
// Backup current state in case the import fails completely on the first move
const backupHistoryFEN = JSON.parse(JSON.stringify(historyFEN));
const backupBranch = [...currentBranch];
const backupStepIndex = currentStepIndex;
// Normalize Chinese notations
text = text.replace(/车/g, '車').replace(/马/g, '馬').replace(/帅/g, '帥').replace(/将/g, '將').replace(/后/g, '後').replace(/进/g, '進');
text = text.replace(/ r(\n|\s|$)/g, ' w$1');
// Handle URL format directly if pasted
if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
let hashPart = text.includes('#') ? text.split('#')[1] : text;
if (hashPart) {
try {
let parsedTree = parseUrlHash(hashPart);
if (parsedTree && parsedTree.fen) {
historyFEN = parsedTree;
initBranch();
currentStepIndex = 0;
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length >= 4) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
}
} catch (e) {
console.log("Failed to parse as URL parameter, falling back to text notation.", e);
}
}
}
// Handle Direct JSON structure
if (text.startsWith('{')) {
try {
const data = JSON.parse(text);
function expand(node, parentFen) {
let dc, simfen, newNode;
if (node.m) {
dc = deriveCoordsFromMove(parentFen, node.m);
simfen = simulateMove(parentFen, dc);
newNode = {
fen: simfen,
move: node.m,
lastMove: dc,
c: node.c || "",
v: []
};
} else {
newNode = {
fen: parentFen,
c: node.c || "",
v: []
};
}
if (node.v) {
newNode.v = node.v.map(child => expand(child, newNode.fen));
}
return newNode;
}
historyFEN = expand(data, data.fen);
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length === 6) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
initBranch(); // initBranch correctly builds currentBranch based on forks
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
} catch (e) {
console.log("JSON parse failed, processing as text notation.");
}
}
// 1. Locate explicitly provided FEN if present
let importedFen = null;
const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
const fenMatch = text.match(fenRegex);
let movesStr = text;
if (fenMatch) {
importedFen = fenMatch[0];
// Strip the FEN string declaration so it isn't parsed as moves later
const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
movesStr = movesStr.replace(cleanRegex, '').trim();
}
// 2. Identify the Attach Point (Parent Node)
let attachIndex = currentStepIndex;
let attachNode = null;
if (importedFen) {
// If FEN exists, trace backwards to find a matching state
for (let i = currentStepIndex; i >= 0; i--) {
const node = getNodeAtStep(i);
if (node && node.fen === importedFen) {
attachIndex = i;
attachNode = node;
break;
}
}
// No match down to root -> Overwrite everything
if (!attachNode) {
attachIndex = 0;
attachNode = {
fen: importedFen,
move: null,
lastMove: null,
c: "",
v: []
};
historyFEN = attachNode;
currentBranch = [];
}
} else {
// No FEN -> Default attach point is the current active step
attachNode = getNodeAtStep(attachIndex);
importedFen = attachNode.fen;
}
// 3. Process move sequence text
// Rule 1: Split text by \n and trim to form an array.
let lines = movesStr.split('\n').map(l => l.trim());
// Check if any line matches the "^\d+\." pattern to determine the parsing mode.
let isComplexFormat = lines.some(l => /^\d+\./.test(l));
let currentFen = importedFen;
let vMap = buildVirtualMap(currentFen);
let currNode = attachNode;
let currentCommentNode = attachNode;
// Calculate how many forks exist from root up to attachIndex
let forkCount = 0;
let tempNode = historyFEN;
for (let i = 0; i < attachIndex; i++) {
if (tempNode.v && tempNode.v.length > 1) {
forkCount++;
}
const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
tempNode = tempNode.v[choice];
}
// Truncate currentBranch to discard choices beyond the attach point
currentBranch = currentBranch.slice(0, forkCount);
let hasError = false;
let isFirstMoveAttempt = true; // Flag to track the first move parsing attempt
if (isComplexFormat) {
let branchMap = {};
let seenCircles = new Set();
let lastMoveParent = attachNode;
let lastMoveParentFen = importedFen;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (!line) continue;
if (!/^\d+\./.test(line)) {
// Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
let isBranchDecl = false;
// Rule 4 & 5: Find the branch insertion point (second occurrence)
for (let c of circles) {
if (seenCircles.has(c) && branchMap[c]) {
currNode = branchMap[c].parentNode;
currentFen = branchMap[c].fen;
vMap = buildVirtualMap(currentFen);
currentCommentNode = currNode;
isBranchDecl = true;
delete branchMap[c]; // Only care about the first two occurrences
break; // Switch context once per line
}
}
// Accumulate comments
if (!isBranchDecl) {
if (!currentCommentNode.c) currentCommentNode.c = "";
currentCommentNode.c += (currentCommentNode.c ? "\n" : "") + line;
}
} else {
// Rule 1 & 2: Line starting with "^\d+\.", strip the number, then split by spaces
let cleanLine = line.replace(/^\d+\./, '').trim();
let tokens = cleanLine.split(/\s+/);
for (let token of tokens) {
// Ignore standalone ellipses
if (token === '...' || token === '…') continue;
let circles = token.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
// Strip circled numbers and ellipses to get the pure move string
let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]/g, '');
if (moveStr.length >= 4) {
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);
if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
let startX = moveStr.charCodeAt(0) - 97;
let startY = 9 - parseInt(moveStr.charAt(1), 10);
let endX = moveStr.charCodeAt(2) - 97;
let endY = 9 - parseInt(moveStr.charAt(3), 10);
dc = { startX, startY, endX, endY };
let pId = vMap.get(`${startX},${startY}`);
if (!pId) {
hasError = true;
continue;
}
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
if (isFirstMoveAttempt) {
// If the very first move fails, try it with INITIAL_FEN
let testIsRed = INITIAL_FEN.includes(' w');
let testMoveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, testIsRed);
let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);
if (!testDc) {
// Fails even on INITIAL_FEN, abort import completely
historyFEN = backupHistoryFEN;
currentBranch = backupBranch;
currentStepIndex = backupStepIndex;
console.warn("Import aborted: Failed to parse the first move token.");
return;
} else {
// Success with INITIAL_FEN. Reset the game to "New Game" state.
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
attachIndex = 0;
// add root comment
for (let j = 0; j < lines.length; j++) {
let line = lines[j];
if (!/^\d+\./.test(line)) {
historyFEN.c += (historyFEN.c === "" ? "" : "\n") + line;
} else {
break;
}
}
// Re-initialize parsing traversal variables
attachNode = historyFEN;
currNode = historyFEN;
currentCommentNode = historyFEN;
currentFen = INITIAL_FEN;
vMap = buildVirtualMap(currentFen);
forkCount = 0;
lastMoveParent = historyFEN;
lastMoveParentFen = INITIAL_FEN;
// Apply the successful test data
dc = testDc;
isRed = testIsRed;
moveEn = testMoveEn;
}
} else {
// If coordinates cannot be derived and it's not the first move, treat as a comment
if (!currentCommentNode.c) {
currentCommentNode.c = "";
}
currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
continue; // Skip node creation and continue parsing
}
}
// Flag that we have successfully processed at least one valid move
isFirstMoveAttempt = false;
// Rule 4: The variation is attached to the parent of the move
lastMoveParent = currNode;
lastMoveParentFen = currentFen;
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = {
fen: nextFen,
move: moveEn,
lastMove: dc,
c: "",
v: []
};
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
// If this creates a fork on the main path, update currentBranch
if (currNode.v.length > 1 && currNode === attachNode) {
currentBranch[forkCount] = childIdx;
forkCount++;
attachNode = currNode.v[childIdx]; // Follow the first parsed line as the main path
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
currentCommentNode = currNode;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
// Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
for (let c of circles) {
if (!seenCircles.has(c)) {
seenCircles.add(c);
// Map the first occurrence to the parent node of the currently parsed move
branchMap[c] = {
parentNode: lastMoveParent,
fen: lastMoveParentFen
};
}
}
}
}
}
} else {
// Fallback for simple flat formats (no "^\d+\." format detected)
let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
let tokensStartIdx = 0;
for (let i = 0; i < tokens.length; i++) {
if (/^1\./.test(tokens[i])) {
tokensStartIdx = i;
break;
}
}
for (let i = tokensStartIdx; i < tokens.length; i++) {
let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);
if (/^[a-i][0-9][a-i][0-9]$/.test(token)) {
let startX = token.charCodeAt(0) - 97;
let startY = 9 - parseInt(token.charAt(1), 10);
let endX = token.charCodeAt(2) - 97;
let endY = 9 - parseInt(token.charAt(3), 10);
dc = { startX, startY, endX, endY };
let pId = vMap.get(`${startX},${startY}`);
if (!pId) {
hasError = true;
break;
}
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
if (!dc) {
if (isFirstMoveAttempt) {
// Try with INITIAL_FEN
let testIsRed = INITIAL_FEN.includes(' w');
let testMoveEn = isEnglish ? token : NotationConverter.toEnglish(token, testIsRed);
let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);
if (!testDc) {
// Fails completely, restore state
historyFEN = backupHistoryFEN;
currentBranch = backupBranch;
currentStepIndex = backupStepIndex;
console.warn("Import aborted: Failed to parse the first move token in simple format.");
return;
} else {
// Reset state to New Game
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
attachNode = historyFEN;
currNode = historyFEN;
currentFen = INITIAL_FEN;
vMap = buildVirtualMap(currentFen);
forkCount = 0;
dc = testDc;
isRed = testIsRed;
moveEn = testMoveEn;
}
} else {
hasError = true;
break;
}
}
}
isFirstMoveAttempt = false;
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = {
fen: nextFen,
move: moveEn,
lastMove: dc,
c: "",
v: []
};
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
if (currNode.v.length > 1) {
currentBranch[forkCount] = childIdx;
forkCount++;
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
}
if (hasError) {
alert("Parsed partially due to an invalid move notation.");
}
// Fill default choices (0) for any remaining branches down the tree
let followNode = attachNode;
let followIdx = 0;
while (followNode && followIdx < currentBranch.length) {
if (followNode.v && followNode.v.length > 1) {
followNode = followNode.v[currentBranch[followIdx]];
} else if (followNode.v && followNode.v.length === 1) {
followNode = followNode.v[0];
} else {
break;
}
followIdx++;
}
while (followNode && followNode.v && followNode.v.length > 0) {
if (followNode.v.length > 1) {
currentBranch.push(0);
}
followNode = followNode.v[0];
}
currentStepIndex = attachIndex;
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
updateToolHighlights();
}
之前isFirstMoveAttempt的意義為「第一個token就轉換失敗」,現在要改為「不是最後一個token轉換失敗」。名稱可能要從isFirstMoveAttempt改成其它的單字。
給出完整的importExportedText函式,所有的代碼和注釋都要使用英文。代码: 全选
/**
* Imports game notation from a string.
* Supports URL hashes, JSON trees, FEN strings, and text notations (Complex or Simple).
*/
function importExportedText(text) {
text = text.trim();
if (!text) {
renderRecordUI();
renderNoteUI();
updateToolHighlights();
return;
}
// Backup current state in case the import fails or needs to be reverted
const backupHistoryFEN = JSON.parse(JSON.stringify(historyFEN));
const backupBranch = [...currentBranch];
const backupStepIndex = currentStepIndex;
// Normalize Chinese characters to standard Traditional Chinese used in logic
text = text.replace(/车/g, '車').replace(/马/g, '馬').replace(/帅/g, '帥')
.replace(/将/g, '將').replace(/后/g, '後').replace(/进/g, '進');
// Standardize turn indicator
text = text.replace(/ r(\n|\s|$)/g, ' w$1');
// Handle URL format directly if pasted (contains hash or specific protocol prefixes)
if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
let hashPart = text.includes('#') ? text.split('#')[1] : text;
if (hashPart) {
try {
let parsedTree = parseUrlHash(hashPart);
if (parsedTree && parsedTree.fen) {
historyFEN = parsedTree;
initBranch();
currentStepIndex = 0;
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length >= 4) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
}
} catch (e) {
console.log("Failed to parse as URL parameter, falling back to text notation.", e);
}
}
}
// Handle Direct JSON structure
if (text.startsWith('{')) {
try {
const data = JSON.parse(text);
function expand(node, parentFen) {
let dc, simfen, newNode;
if (node.m) {
dc = deriveCoordsFromMove(parentFen, node.m);
simfen = simulateMove(parentFen, dc);
newNode = {
fen: simfen,
move: node.m,
lastMove: dc,
c: node.c || "",
v: []
};
} else {
newNode = {
fen: parentFen,
c: node.c || "",
v: []
};
}
if (node.v) {
newNode.v = node.v.map(child => expand(child, newNode.fen));
}
return newNode;
}
historyFEN = expand(data, data.fen);
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length === 6) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
initBranch();
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
} catch (e) {
console.log("JSON parse failed, processing as text notation.");
}
}
// 1. Locate explicitly provided FEN if present
let importedFen = null;
const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
const fenMatch = text.match(fenRegex);
let movesStr = text;
if (fenMatch) {
importedFen = fenMatch[0];
// Remove FEN string and keywords so they aren't parsed as moves
const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
movesStr = movesStr.replace(cleanRegex, '').trim();
}
// 2. Identify the Attach Point (Parent Node)
let attachIndex = currentStepIndex;
let attachNode = null;
if (importedFen) {
// Trace backwards to find a matching state in history
for (let i = currentStepIndex; i >= 0; i--) {
const node = getNodeAtStep(i);
if (node && node.fen === importedFen) {
attachIndex = i;
attachNode = node;
break;
}
}
// No match found -> Overwrite history from this FEN
if (!attachNode) {
attachIndex = 0;
attachNode = {
fen: importedFen,
move: null,
lastMove: null,
c: "",
v: []
};
historyFEN = attachNode;
currentBranch = [];
}
} else {
// No FEN found -> Attach to current step
attachNode = getNodeAtStep(attachIndex);
importedFen = attachNode.fen;
}
// 3. Process move sequence text
let lines = movesStr.split('\n').map(l => l.trim()).filter(l => l.length > 0);
let isComplexFormat = lines.some(l => /^\d+\./.test(l));
let currentFen = importedFen;
let vMap = buildVirtualMap(currentFen);
let currNode = attachNode;
let currentCommentNode = attachNode;
// Calculate existing fork choices up to attachIndex
let forkCount = 0;
let tempNode = historyFEN;
for (let i = 0; i < attachIndex; i++) {
if (tempNode.v && tempNode.v.length > 1) {
forkCount++;
}
const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
tempNode = tempNode.v[choice];
}
currentBranch = currentBranch.slice(0, forkCount);
let hasError = false;
if (isComplexFormat) {
let branchMap = {};
let seenCircles = new Set();
let lastMoveParent = attachNode;
let lastMoveParentFen = importedFen;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (!/^\d+\./.test(line)) {
// Line is a comment or branch declaration
let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
let isBranchDecl = false;
for (let c of circles) {
if (seenCircles.has(c) && branchMap[c]) {
currNode = branchMap[c].parentNode;
currentFen = branchMap[c].fen;
vMap = buildVirtualMap(currentFen);
currentCommentNode = currNode;
isBranchDecl = true;
delete branchMap[c];
break;
}
}
if (!isBranchDecl) {
if (!currentCommentNode.c) currentCommentNode.c = "";
currentCommentNode.c += (currentCommentNode.c ? "\n" : "") + line;
}
} else {
let cleanLine = line.replace(/^\d+\./, '').trim();
let tokens = cleanLine.split(/\s+/).filter(t => t.length > 0);
for (let j = 0; j < tokens.length; j++) {
let token = tokens[j];
if (token === '...' || token === '…') continue;
let circles = token.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]/g, '');
// Logic: If this is NOT the last token in the entire text, we consider it a candidate for reset
const isNotLastMoveToken = (i < lines.length - 1) || (j < tokens.length - 1);
if (moveStr.length >= 4) {
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);
if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
let startX = moveStr.charCodeAt(0) - 97;
let startY = 9 - parseInt(moveStr.charAt(1), 10);
let endX = moveStr.charCodeAt(2) - 97;
let endY = 9 - parseInt(moveStr.charAt(3), 10);
dc = { startX, startY, endX, endY };
let pId = vMap.get(`${startX},${startY}`);
if (!pId) dc = null; // Mark as failure
else moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
// Failure logic: If it's not the last token, try restarting from Initial Position
if (isNotLastMoveToken) {
let testIsRed = INITIAL_FEN.includes(' w');
let testMoveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, testIsRed);
let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);
if (!testDc) {
// Complete failure
historyFEN = backupHistoryFEN;
currentBranch = backupBranch;
currentStepIndex = backupStepIndex;
console.warn("Import aborted: Critical failure in move sequence.");
return;
} else {
// Success with INITIAL_FEN. Reset game state.
historyFEN = { fen: INITIAL_FEN, move: null, lastMove: null, c: "", v: [] };
currentBranch = [];
currentStepIndex = 0;
attachIndex = 0;
// Re-parse leading lines as root comments
for (let k = 0; k < lines.length; k++) {
if (!/^\d+\./.test(lines[k])) historyFEN.c += (historyFEN.c ? "\n" : "") + lines[k];
else break;
}
attachNode = currNode = currentCommentNode = lastMoveParent = historyFEN;
currentFen = lastMoveParentFen = INITIAL_FEN;
vMap = buildVirtualMap(currentFen);
forkCount = 0;
dc = testDc; isRed = testIsRed; moveEn = testMoveEn;
}
} else {
// Last token failed, treat as comment
currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
continue;
}
}
lastMoveParent = currNode;
lastMoveParentFen = currentFen;
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
currNode.v.push({ fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] });
childIdx = currNode.v.length - 1;
}
if (currNode.v.length > 1 && currNode === attachNode) {
currentBranch[forkCount] = childIdx;
forkCount++;
attachNode = currNode.v[childIdx];
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
currentCommentNode = currNode;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
for (let c of circles) {
if (!seenCircles.has(c)) {
seenCircles.add(c);
branchMap[c] = { parentNode: lastMoveParent, fen: lastMoveParentFen };
}
}
}
}
}
} else {
// Fallback for Simple/Flat formats
let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
let tokensStartIdx = 0;
for (let i = 0; i < tokens.length; i++) {
if (/^1\./.test(tokens[i])) { tokensStartIdx = i; break; }
}
for (let i = tokensStartIdx; i < tokens.length; i++) {
let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
const isNotLastMoveToken = i < tokens.length - 1;
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);
if (/^[a-i][0-9][a-i][0-9]$/.test(token)) {
let startX = token.charCodeAt(0) - 97;
let startY = 9 - parseInt(token.charAt(1), 10);
let endX = token.charCodeAt(2) - 97;
let endY = 9 - parseInt(token.charAt(3), 10);
dc = { startX, startY, endX, endY };
let pId = vMap.get(`${startX},${startY}`);
if (!pId) dc = null;
else moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
if (isNotLastMoveToken) {
let testIsRed = INITIAL_FEN.includes(' w');
let testMoveEn = isEnglish ? token : NotationConverter.toEnglish(token, testIsRed);
let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);
if (!testDc) {
historyFEN = backupHistoryFEN;
currentBranch = backupBranch;
currentStepIndex = backupStepIndex;
console.warn("Import aborted: Failure in simple format move sequence.");
return;
} else {
historyFEN = { fen: INITIAL_FEN, move: null, lastMove: null, c: "", v: [] };
currentBranch = [];
currentStepIndex = 0;
attachNode = currNode = historyFEN;
currentFen = INITIAL_FEN;
vMap = buildVirtualMap(currentFen);
forkCount = 0;
dc = testDc; isRed = testIsRed; moveEn = testMoveEn;
}
} else {
hasError = true;
break;
}
}
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
currNode.v.push({ fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] });
childIdx = currNode.v.length - 1;
}
if (currNode.v.length > 1) {
currentBranch[forkCount] = childIdx;
forkCount++;
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
}
if (hasError) alert("Parsed partially due to an invalid move notation.");
// Resolve default choices for remaining branches down the tree
let followNode = attachNode;
let followIdx = 0;
while (followNode && followIdx < currentBranch.length) {
if (followNode.v && followNode.v.length > 0) {
let idx = currentBranch[followIdx] || 0;
followNode = followNode.v[idx] || followNode.v[0];
} else break;
followIdx++;
}
while (followNode && followNode.v && followNode.v.length > 0) {
if (followNode.v.length > 1) currentBranch.push(0);
followNode = followNode.v[0];
}
currentStepIndex = attachIndex;
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
updateToolHighlights();
}代码: 全选
改進importExportedText函式:
```
function importExportedText(text) {
text = text.trim();
if (!text) {
renderRecordUI();
renderNoteUI();
updateToolHighlights();
return;
}
// Normalize Chinese notations
text = text.replace(/车/g, '車').replace(/马/g, '馬').replace(/帅/g, '帥').replace(/将/g, '將').replace(/后/g, '後').replace(/进/g, '進');
text = text.replace(/ r(\n|\s|$)/g, ' w$1');
// Handle URL format directly if pasted
if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
let hashPart = text.includes('#') ? text.split('#')[1] : text;
if (hashPart) {
try {
let parsedTree = parseUrlHash(hashPart);
if (parsedTree && parsedTree.fen) {
historyFEN = parsedTree;
initBranch();
currentStepIndex = 0;
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length >= 4) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
}
} catch (e) {
console.log("Failed to parse as URL parameter, falling back to text notation.", e);
}
}
}
// Handle Direct JSON structure
if (text.startsWith('{')) {
try {
const data = JSON.parse(text);
function expand(node, parentFen) {
let dc, simfen, newNode;
if (node.m) {
dc = deriveCoordsFromMove(parentFen, node.m);
simfen = simulateMove(parentFen, dc);
newNode = {
fen: simfen,
move: node.m,
lastMove: dc,
c: node.c || "",
v: []
};
} else {
newNode = {
fen: parentFen,
c: node.c || "",
v: []
};
}
if (node.v) {
newNode.v = node.v.map(child => expand(child, newNode.fen));
}
return newNode;
}
historyFEN = expand(data, data.fen);
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length === 6) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
initBranch(); // initBranch correctly builds currentBranch based on forks
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
} catch (e) {
console.log("JSON parse failed, processing as text notation.");
}
}
// 1. Locate explicitly provided FEN if present
let importedFen = null;
const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
const fenMatch = text.match(fenRegex);
let movesStr = text;
if (fenMatch) {
importedFen = fenMatch[0];
// Strip the FEN string declaration so it isn't parsed as moves later
const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
movesStr = movesStr.replace(cleanRegex, '').trim();
}
// 2. Identify the Attach Point (Parent Node)
let attachIndex = currentStepIndex;
let attachNode = null;
if (importedFen) {
// If FEN exists, trace backwards to find a matching state
for (let i = currentStepIndex; i >= 0; i--) {
const node = getNodeAtStep(i);
if (node && node.fen === importedFen) {
attachIndex = i;
attachNode = node;
break;
}
}
// No match down to root -> Overwrite everything
if (!attachNode) {
attachIndex = 0;
attachNode = {
fen: importedFen,
move: null,
lastMove: null,
c: "",
v: []
};
historyFEN = attachNode;
currentBranch = [];
}
} else {
// No FEN -> Default attach point is the current active step
attachNode = getNodeAtStep(attachIndex);
importedFen = attachNode.fen;
}
// 3. Process move sequence text
// Rule 1: Split text by \n and trim to form an array.
let lines = movesStr.split('\n').map(l => l.trim());
// Check if any line matches the "^\d+\." pattern to determine the parsing mode.
let isComplexFormat = lines.some(l => /^\d+\./.test(l));
let currentFen = importedFen;
let vMap = buildVirtualMap(currentFen);
let currNode = attachNode;
let currentCommentNode = attachNode;
// Calculate how many forks exist from root up to attachIndex
let forkCount = 0;
let tempNode = historyFEN;
for (let i = 0; i < attachIndex; i++) {
if (tempNode.v && tempNode.v.length > 1) {
forkCount++;
}
const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
tempNode = tempNode.v[choice];
}
// Truncate currentBranch to discard choices beyond the attach point
currentBranch = currentBranch.slice(0, forkCount);
let hasError = false;
if (isComplexFormat) {
let branchMap = {};
let seenCircles = new Set();
let lastMoveParent = attachNode;
let lastMoveParentFen = importedFen;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (!line) continue;
if (!/^\d+\./.test(line)) {
// Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
let isBranchDecl = false;
// Rule 4 & 5: Find the branch insertion point (second occurrence)
for (let c of circles) {
if (seenCircles.has(c) && branchMap[c]) {
currNode = branchMap[c].parentNode;
currentFen = branchMap[c].fen;
vMap = buildVirtualMap(currentFen);
currentCommentNode = currNode;
isBranchDecl = true;
delete branchMap[c]; // Only care about the first two occurrences
break; // Switch context once per line
}
}
// Accumulate comments
if (!isBranchDecl) {
if (!currentCommentNode.c) currentCommentNode.c = "";
currentCommentNode.c += (currentCommentNode.c ? "\n" : "") + line;
}
} else {
// Rule 1 & 2: Line starting with "^\d+\.", strip the number, then split by spaces
let cleanLine = line.replace(/^\d+\./, '').trim();
let tokens = cleanLine.split(/\s+/);
for (let token of tokens) {
// Ignore standalone ellipses
if (token === '...' || token === '…') continue;
let circles = token.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
// Strip circled numbers and ellipses to get the pure move string
let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]/g, '');
if (moveStr.length >= 4) {
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
let startX = moveStr.charCodeAt(0) - 97;
let startY = 9 - parseInt(moveStr.charAt(1), 10);
let endX = moveStr.charCodeAt(2) - 97;
let endY = 9 - parseInt(moveStr.charAt(3), 10);
dc = {
startX,
startY,
endX,
endY
};
let pId = vMap.get(`${startX},${startY}`);
if (!pId) {
hasError = true;
continue;
}
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);
moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
// If coordinates cannot be derived, treat the token as a comment
// for the current node (which represents the previous step)
if (!currentCommentNode.c) {
currentCommentNode.c = "";
}
// Append the token, separating with a space if a comment already exists
currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
continue; // Skip node creation and continue parsing
}
// Rule 4: The variation is attached to the parent of the move
lastMoveParent = currNode;
lastMoveParentFen = currentFen;
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = {
fen: nextFen,
move: moveEn,
lastMove: dc,
c: "",
v: []
};
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
// If this creates a fork on the main path, update currentBranch
if (currNode.v.length > 1 && currNode === attachNode) {
currentBranch[forkCount] = childIdx;
forkCount++;
attachNode = currNode.v[childIdx]; // Follow the first parsed line as the main path
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
currentCommentNode = currNode;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
// Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
for (let c of circles) {
if (!seenCircles.has(c)) {
seenCircles.add(c);
// Map the first occurrence to the parent node of the currently parsed move
branchMap[c] = {
parentNode: lastMoveParent,
fen: lastMoveParentFen
};
}
}
}
}
}
} else {
// Fallback for simple flat formats (no "^\d+\." format detected)
let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
let tokensStartIdx = 0;
for (let i = 0; i < tokens.length; i++) {
if (/^1\./.test(tokens[i])) {
tokensStartIdx = i;
break;
}
}
for (let i = tokensStartIdx; i < tokens.length; i++) {
let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
if (/^[a-i][0-9][a-i][0-9]$/.test(token)) {
let startX = token.charCodeAt(0) - 97;
let startY = 9 - parseInt(token.charAt(1), 10);
let endX = token.charCodeAt(2) - 97;
let endY = 9 - parseInt(token.charAt(3), 10);
dc = {
startX,
startY,
endX,
endY
};
let pId = vMap.get(`${startX},${startY}`);
if (!pId) {
hasError = true;
break;
}
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);
moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
if (!dc) {
hasError = true;
break;
}
}
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = {
fen: nextFen,
move: moveEn,
lastMove: dc,
c: "",
v: []
};
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
if (currNode.v.length > 1) {
currentBranch[forkCount] = childIdx;
forkCount++;
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
}
if (hasError) {
alert("Parsed partially due to an invalid move notation.");
}
// Fill default choices (0) for any remaining branches down the tree
let followNode = attachNode;
let followIdx = 0;
while (followNode && followIdx < currentBranch.length) {
if (followNode.v && followNode.v.length > 1) {
followNode = followNode.v[currentBranch[followIdx]];
} else if (followNode.v && followNode.v.length === 1) {
followNode = followNode.v[0];
} else {
break;
}
followIdx++;
}
while (followNode && followNode.v && followNode.v.length > 0) {
if (followNode.v.length > 1) {
currentBranch.push(0);
}
followNode = followNode.v[0];
}
currentStepIndex = attachIndex;
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
updateToolHighlights();
}
```
在這一段:「
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
// If coordinates cannot be derived, treat the token as a comment
// for the current node (which represents the previous step)
if (!currentCommentNode.c) {
currentCommentNode.c = "";
}
// Append the token, separating with a space if a comment already exists
currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
continue; // Skip node creation and continue parsing
}
」
它的意義為,如果用deriveCoordsFromMove轉換失敗,則token將成為上一步的評論。
現在加上一個判斷,如果「不是分支的最後一個token」轉換失敗,則嘗試「新建局面」導入,如果在新建局面下仍然「不是分支的最後一個token」轉換失敗,則取消導入,恢復為導入之前的狀態。只有「分支的最後一個token」才可能成為上一步的評論。
新建局面主要影響的是這三個變量:
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
下面舉例說明什麼是「分支的最後一個token」:
```
标题: 15 兑子抢先 丝丝入扣
分类: 象棋谱大全-现代棋书专集
赛事: 中局大全
轮次: 战术精华
结果: 黑方胜
日期: 0000.00.00
评论: 讲评: 郭有志
作者: 文件作者: 郭有志
备注:
棋局类型: 全局
棋局性质: 慢棋
棋谱主人: 象棋谱大全
棋谱价值: 1
浏览次数: 7930
来源网站: www.dpxq.com
初始局面:
黑方
┌-[车][象][士][将][士]-┬-[车]-┐
│ │ │ │\│/│ │ │ │
├─┼─┼─┼─※─┼─┼─┼─┤
│ │ │ │/│\│ │ │ │
├─┼-[马]-┼-[象]-┼-[炮]-┼─┤
│ │ │ │ │ │ │ │ │
[卒]-┼-[卒]-┼-[卒]-┼─┼─┼-[卒]
│ │ │ │ │ │ │ │ │
├─┴─┴─┴─┴─┴─┴─┴─┤
│ │
├─┬-(兵)-┬─┬─┬-(车)-┬─┤
│ │ │ │ │ │ │ │ │
(兵)-┼-[炮]-┼-(兵)-┼─┼─┼-(兵)
│ │ │ │ │ │ │ │ │
├-(炮)(炮)-┼-(相)-┼-(马)-┼─┤
│ │ │ │\│/│ │ │ │
├─┼─┼─┼─※─┼─┼─┼─┤
│ │ │ │/│\│ │ │ │
└-(车)-┴-(士)(帅)(士)(相)-┴─┘
红方
【主变: 黑胜】
1. 炮7进5
兑子抢先,车占兵林,力在其中。
1. ………… 炮七平三
2. 车8进6 车三平五 ①
3. 车2进4
高车,护卒攻车。
3. ………… 仕六进五
4. 卒5进1 车五平六
5. 马3进5
跃马,否则红有车6进2,再平7制马之棋。
5. ………… 车八平六
6. 士6进5
必行之着!
6. ………… 炮八平七 ②
7. 马5进7
马跃进河口,下着有卒5进1。
7. ………… 前车平三
8. 马7退6 车六进三
9. 炮3平5
黑方运子推进,着法丝丝入扣。炮打中兵,确立优势。
9. ………… 帅五平六
10. 卒5进1
强冲中卒,好!红车不敢吃之。
10. ………… 相五退七 ③
11. 车2平8 炮七平五 ④
12. 炮5平1 车六进五
13. 前车平5 炮五进二
14. 车8平5 炮五进三 ⑤
弃炮无奈。
15. 后车退2 炮三平五
16. 炮1平9 黑胜势!!
变①接主变第2回合
2. ………… 兵五进一
3. 车2进6 兵九进一
4. 炮3平9 炮三平一
5. 车8退3 黑优。
变②接主变第6回合
6. ………… 兵五进一
7. 马5进7 兵五进一
8. 马7进6 前车平四
9. 车2平5 黑优!
变③接主变第10回合
10. ………… 车三平五
11. 炮5进2 车六平二 ⑥
12. 炮5退3 黑优!!
变④接主变第11回合
11. ………… 车三平五
12. 炮5平1 车六平二
13. 车8进2 兵一进一
14. 炮1退1 兵七进一
15. 炮1进4 相七进九
16. 卒3进1
变⑤接主变第14回合
14. ………… 炮五平四
15. 炮1进3 相七进五
16. 后车平2
变⑥接变③第11回合
11. ………… 车五平六
12. 车8平4 车六退一
13. 炮5平9
棋谱由 http://www.dpxq.com/ 生成
```
在上面的棋譜文本中,「黑胜势!!」、「黑优。」、「黑优!」、「黑优!!」、「卒3进1」、「后车平2」、「炮5平9」都是分支的最後一個token。其中,「黑胜势!!」、「黑优。」、「黑优!」、「黑优!!」將成為上一步的評論。
同時,要首先通過英文棋譜來判斷是否能正確轉換,在英文棋譜的前四個字符中,第三個一定是[+-=」的其中一個,第四個一定是數字[1-9],前兩個一定是[RNBAKCPrnbakcp+-=1-9]的其中一個。
給出完整的importExportedText函式,所有代碼和注釋都要使用英文。代码: 全选
function importExportedText(text) {
// Backup current state (for rollback)
const backup = {
historyFEN: JSON.parse(JSON.stringify(historyFEN)),
currentBranch: [...currentBranch],
currentStepIndex
};
function restoreBackup() {
historyFEN = backup.historyFEN;
currentBranch = backup.currentBranch;
currentStepIndex = backup.currentStepIndex;
}
// Check if token looks like valid English notation
function isLikelyEnglishMove(str) {
if (str.length < 4) return false;
const s = str.substring(0, 4);
return /^[RNBAKCPrnbakcp+\-=1-9]{2}[+\-=][1-9]$/.test(s);
}
text = text.trim();
if (!text) {
renderRecordUI();
renderNoteUI();
updateToolHighlights();
return;
}
// Normalize Chinese
text = text.replace(/车/g, '車').replace(/马/g, '馬')
.replace(/帅/g, '帥').replace(/将/g, '將')
.replace(/后/g, '後').replace(/进/g, '進');
text = text.replace(/ r(\n|\s|$)/g, ' w$1');
// --- URL format ---
if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
let hashPart = text.includes('#') ? text.split('#')[1] : text;
if (hashPart) {
try {
let parsedTree = parseUrlHash(hashPart);
if (parsedTree && parsedTree.fen) {
historyFEN = parsedTree;
initBranch();
currentStepIndex = 0;
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length >= 4) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
}
} catch (e) {}
}
}
// --- JSON format ---
if (text.startsWith('{')) {
try {
const data = JSON.parse(text);
function expand(node, parentFen) {
let dc, simfen, newNode;
if (node.m) {
dc = deriveCoordsFromMove(parentFen, node.m);
simfen = simulateMove(parentFen, dc);
newNode = {
fen: simfen,
move: node.m,
lastMove: dc,
c: node.c || "",
v: []
};
} else {
newNode = {
fen: parentFen,
c: node.c || "",
v: []
};
}
if (node.v) {
newNode.v = node.v.map(child => expand(child, newNode.fen));
}
return newNode;
}
historyFEN = expand(data, data.fen);
initBranch();
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
} catch (e) {}
}
// --- FEN detection ---
let importedFen = null;
const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
const fenMatch = text.match(fenRegex);
let movesStr = text;
if (fenMatch) {
importedFen = fenMatch[0];
movesStr = movesStr.replace(fenRegex, '').trim();
}
let attachNode;
let attachIndex = currentStepIndex;
if (importedFen) {
attachNode = {
fen: importedFen,
move: null,
lastMove: null,
c: "",
v: []
};
historyFEN = attachNode;
currentBranch = [];
attachIndex = 0;
} else {
attachNode = getNodeAtStep(attachIndex);
importedFen = attachNode.fen;
}
let lines = movesStr.split('\n').map(l => l.trim());
let isComplex = lines.some(l => /^\d+\./.test(l));
let currentFen = importedFen;
let vMap = buildVirtualMap(currentFen);
let currNode = attachNode;
let currentCommentNode = attachNode;
function tryReparseFromInitial(token, isLastToken) {
if (isLastToken) return false;
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
currentFen = INITIAL_FEN;
vMap = buildVirtualMap(currentFen);
currNode = historyFEN;
let isRed = currentFen.includes(' w');
let moveEn = isLikelyEnglishMove(token)
? token
: NotationConverter.toEnglish(token, isRed);
let dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
return !!dc;
}
let hasError = false;
if (isComplex) {
for (let line of lines) {
if (!line) continue;
if (!/^\d+\./.test(line)) {
currentCommentNode.c += (currentCommentNode.c ? "\n" : "") + line;
continue;
}
let tokens = line.replace(/^\d+\./, '').trim().split(/\s+/);
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
let isLastToken = (i === tokens.length - 1);
if (token === '...' || token === '…') continue;
let moveStr = token.replace(/[①-⑳…\.]/g, '');
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
let sx = moveStr.charCodeAt(0) - 97;
let sy = 9 - parseInt(moveStr[1]);
let ex = moveStr.charCodeAt(2) - 97;
let ey = 9 - parseInt(moveStr[3]);
dc = { startX: sx, startY: sy, endX: ex, endY: ey };
} else {
moveEn = isLikelyEnglishMove(moveStr)
? moveStr
: NotationConverter.toEnglish(moveStr, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
// --- NEW CORE LOGIC ---
if (!dc) {
if (!isLastToken) {
// Try reset
if (!tryReparseFromInitial(token, false)) {
restoreBackup();
alert("Import failed: invalid move.");
return;
}
continue;
} else {
// Only last token becomes comment
currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
continue;
}
}
let nextFen = simulateMove(currentFen, dc);
let newNode = {
fen: nextFen,
move: moveEn,
lastMove: dc,
c: "",
v: []
};
currNode.v.push(newNode);
currNode = newNode;
currentFen = nextFen;
currentCommentNode = currNode;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
}
}
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
updateToolHighlights();
}代码: 全选
function importExportedText(text) {
text = text.trim();
if (!text) {
renderRecordUI();
renderNoteUI();
updateToolHighlights();
return;
}
// Backup current state for potential rollback
const backupState = {
historyFEN: JSON.parse(JSON.stringify(historyFEN)),
currentBranch: [...currentBranch],
currentStepIndex: currentStepIndex
};
const restoreBackup = () => {
historyFEN = backupState.historyFEN;
currentBranch = backupState.currentBranch;
currentStepIndex = backupState.currentStepIndex;
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
updateToolHighlights();
};
// Normalize Chinese notations
text = text.replace(/车/g, '車').replace(/马/g, '馬').replace(/帅/g, '帥').replace(/将/g, '將').replace(/后/g, '後').replace(/进/g, '進');
text = text.replace(/ r(\n|\s|$)/g, ' w$1');
// Handle URL format directly
if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
let hashPart = text.includes('#') ? text.split('#')[1] : text;
if (hashPart) {
try {
let parsedTree = parseUrlHash(hashPart);
if (parsedTree && parsedTree.fen) {
historyFEN = parsedTree;
initBranch();
currentStepIndex = 0;
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length >= 4) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
}
} catch (e) {
console.log("URL parse failed, falling back.");
}
}
}
// Handle Direct JSON structure
if (text.startsWith('{')) {
try {
const data = JSON.parse(text);
function expand(node, parentFen) {
let dc, simfen, newNode;
if (node.m) {
dc = deriveCoordsFromMove(parentFen, node.m);
simfen = simulateMove(parentFen, dc);
newNode = { fen: simfen, move: node.m, lastMove: dc, c: node.c || "", v: [] };
} else {
newNode = { fen: parentFen, c: node.c || "", v: [] };
}
if (node.v) newNode.v = node.v.map(child => expand(child, newNode.fen));
return newNode;
}
historyFEN = expand(data, data.fen);
let fenArr = historyFEN.fen.split(' ');
if (fenArr.length === 6) {
isRotateEnabled = fenArr[2] === '1';
isFlipEnabled = fenArr[3] === '1';
reapplyVisualPositions();
}
initBranch();
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
return;
} catch (e) {
console.log("JSON parse failed.");
}
}
// Parse logic for text notations (Simple/Complex)
let hasAttemptedReset = false;
function performImport(isRetry = false) {
let currentFen = isRetry ? INITIAL_FEN : (importedFen || getNodeAtStep(currentStepIndex).fen);
// If we are retrying from Initial FEN, we reset the history root
if (isRetry) {
historyFEN = { fen: INITIAL_FEN, move: null, lastMove: null, c: "", v: [] };
currentBranch = [];
currentStepIndex = 0;
}
let vMap = buildVirtualMap(currentFen);
let currNode = isRetry ? historyFEN : getNodeAtStep(currentStepIndex);
let attachNode = currNode;
let currentCommentNode = currNode;
let attachIndex = currentStepIndex;
// Calculate forkCount for the current branch path
let forkCount = 0;
let tempNode = historyFEN;
for (let i = 0; i < attachIndex; i++) {
if (tempNode.v && tempNode.v.length > 1) forkCount++;
const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
tempNode = tempNode.v[choice];
}
currentBranch = currentBranch.slice(0, forkCount);
let lines = movesStr.split('\n').map(l => l.trim()).filter(l => l.length > 0);
let isComplexFormat = lines.some(l => /^\d+\./.test(l));
if (isComplexFormat) {
let branchMap = {};
let seenCircles = new Set();
let lastMoveParent = attachNode;
let lastMoveParentFen = currentFen;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (!/^\d+\./.test(line)) {
// Treat non-move lines as comments
let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
let isBranchDecl = false;
for (let c of circles) {
if (seenCircles.has(c) && branchMap[c]) {
currNode = branchMap[c].parentNode;
currentFen = branchMap[c].fen;
vMap = buildVirtualMap(currentFen);
currentCommentNode = currNode;
isBranchDecl = true;
delete branchMap[c];
break;
}
}
if (!isBranchDecl) {
currentCommentNode.c = (currentCommentNode.c ? currentCommentNode.c + "\n" : "") + line;
}
} else {
let cleanLine = line.replace(/^\d+\./, '').trim();
let tokens = cleanLine.split(/\s+/).filter(t => t !== '...' && t !== '…');
for (let j = 0; j < tokens.length; j++) {
let token = tokens[j];
let isLastTokenOfBranch = (j === tokens.length - 1);
let circles = token.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]/g, '');
if (moveStr.length < 4) {
if (isLastTokenOfBranch) {
currentCommentNode.c = (currentCommentNode.c ? currentCommentNode.c + " " : "") + token;
continue;
} else if (!hasAttemptedReset) {
hasAttemptedReset = true;
return performImport(true);
} else {
restoreBackup();
return;
}
}
let dc = null;
let moveEn = "";
let isRed = currentFen.includes(' w');
// Rule: Check for English Notation pattern
// Piece code (2 chars) + Operator [+-=] + Number [1-9]
let isEnglish = /^[RNBAKCPrnbakcp+\-=1-9]{2}[+\-=][1-9]/.test(moveStr);
if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
let startX = moveStr.charCodeAt(0) - 97;
let startY = 9 - parseInt(moveStr.charAt(1), 10);
let endX = moveStr.charCodeAt(2) - 97;
let endY = 9 - parseInt(moveStr.charAt(3), 10);
dc = { startX, startY, endX, endY };
let pId = vMap.get(`${startX},${startY}`);
if (pId) moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
if (isLastTokenOfBranch) {
currentCommentNode.c = (currentCommentNode.c ? currentCommentNode.c + " " : "") + token;
continue;
} else if (!hasAttemptedReset) {
hasAttemptedReset = true;
return performImport(true);
} else {
restoreBackup();
return;
}
}
// Success: Build the move node
lastMoveParent = currNode;
lastMoveParentFen = currentFen;
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = { fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] };
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
if (currNode.v.length > 1 && currNode === attachNode) {
currentBranch[forkCount] = childIdx;
forkCount++;
attachNode = currNode.v[childIdx];
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
currentCommentNode = currNode;
// Update Virtual Map
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
// Map circles for branch declarations
for (let c of circles) {
if (!seenCircles.has(c)) {
seenCircles.add(c);
branchMap[c] = { parentNode: lastMoveParent, fen: lastMoveParentFen };
}
}
}
}
}
} else {
// Simple format parsing
let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i].replace(/^\d+\.\s*/, '');
let isLastToken = (i === tokens.length - 1);
let moveStr = token.substring(0, 4);
let isRed = currentFen.includes(' w');
let dc = null, moveEn = "";
if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
let startX = moveStr.charCodeAt(0) - 97;
let startY = 9 - parseInt(moveStr.charAt(1), 10);
let endX = moveStr.charCodeAt(2) - 97;
let endY = 9 - parseInt(moveStr.charAt(3), 10);
dc = { startX, startY, endX, endY };
let pId = vMap.get(`${startX},${startY}`);
if (pId) moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
let isEnglish = /^[RNBAKCPrnbakcp+\-=1-9]{2}[+\-=][1-9]/.test(token);
moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
if (isLastToken) {
currNode.c = (currNode.c ? currNode.c + " " : "") + token;
continue;
} else if (!hasAttemptedReset) {
hasAttemptedReset = true;
return performImport(true);
} else {
restoreBackup();
return;
}
}
let nextFen = simulateMove(currentFen, dc);
let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
if (childIdx === -1) {
let newNode = { fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] };
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
if (currNode.v.length > 1) {
currentBranch[forkCount] = childIdx;
forkCount++;
}
currNode = currNode.v[childIdx];
currentFen = nextFen;
let pId = vMap.get(`${dc.startX},${dc.startY}`);
vMap.delete(`${dc.startX},${dc.startY}`);
if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
}
}
// Finalize Import
currentStepIndex = attachIndex;
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
updateToolHighlights();
}
// Identify FEN in text
let importedFen = null;
const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
const fenMatch = text.match(fenRegex);
let movesStr = text;
if (fenMatch) {
importedFen = fenMatch[0];
const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
movesStr = movesStr.replace(cleanRegex, '').trim();
}
// Execute import logic
performImport(false);
}代码: 全选
改進importExportedText函式,察看上傳的function.js。
在這一段:
```
dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
}
if (!dc) {
// If coordinates cannot be derived, treat the token as a comment
// for the current node (which represents the previous step)
if (!currentCommentNode.c) {
currentCommentNode.c = "";
}
// Append the token, separating with a space if a comment already exists
currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
continue; // Skip node creation and continue parsing
}
```
它的意義為,如果用deriveCoordsFromMove轉換失敗,則token將成為上一步的評論。
現在加上一個判斷,如果「不是分支的最後一個token」轉換失敗,則嘗試「新建局面」導入,如果在新建局面下仍然「不是分支的最後一個token」轉換失敗,則取消導入,恢復為導入之前的狀態。只有「分支的最後一個token」才可能成為上一步的評論。
新建局面主要影響的是這三個變量:
```
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
```
下面舉例說明什麼是「分支的最後一個token」,察看上傳的dpqp.txt。
在上面的棋譜文本中,「黑胜势!!」、「黑优。」、「黑优!」、「黑优!!」、「卒3进1」、「后车平2」、「炮5平9」都是分支的最後一個token。其中,「黑胜势!!」、「黑优。」、「黑优!」、「黑优!!」將成為上一步的評論。
同時,要首先通過英文棋譜來判斷是否能正確轉換,在英文棋譜的前四個字符中,第三個一定是[+-=」的其中一個,第四個一定是數字[1-9],前兩個一定是[RNBAKCPrnbakcp+-=1-9]的其中一個。
給出完整的importExportedText函式,所有代碼和注釋都要使用英文。