To implement this feature, you only need to replace the importExportedText function. The new logic will detect if the text matches the specific numbered format. If it does, it will parse the file sequentially: applying moves, attaching comments directly to the current node, parsing branch markers ①-⑳, linking them to the parent context of the specified move, and seamlessly jumping back when those markers appear on a standalone line.
代码: 全选
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.");
}
}
// --- NEW: Handle Formatted Text with Branches and Comments ---
// Checks if the format contains numbered move notation (e.g., 1. 炮二平五 or 1. ... 炮8平5)
const isNumberedFormat = /(?:^|\n)\s*\d+\.\s*(?:[.]{3,}|…+|[\u4e00-\u9fa5]+)/.test(text);
if (isNumberedFormat) {
let importedFen = INITIAL_FEN;
const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
const fenMatch = text.match(fenRegex);
let textBody = text;
if (fenMatch) {
importedFen = fenMatch[0];
textBody = textBody.replace(fenMatch[0], ''); // Remove FEN so it isn't parsed as comment
}
// Isolate the root comment from the moves
let cutIndex = -1;
let matchMeta = textBody.match(/【/);
let matchMove = textBody.match(/(?:^|\n)\s*\d+\./);
if (matchMeta && matchMove) {
cutIndex = Math.min(matchMeta.index, matchMove.index);
} else if (matchMeta) {
cutIndex = matchMeta.index;
} else if (matchMove) {
cutIndex = matchMove.index;
}
let rootComment = "";
if (cutIndex > 0) {
rootComment = textBody.substring(0, cutIndex).trim();
}
if (cutIndex !== -1) {
textBody = textBody.substring(cutIndex);
}
textBody = textBody.replace(/【.*?】/g, ''); // Strip metadata
let attachNode = {
fen: importedFen,
move: null,
lastMove: null,
c: rootComment,
v: []
};
historyFEN = attachNode;
currentBranch = [];
let currNode = attachNode;
let currentFen = importedFen;
let branchMap = {}; // Maps circled numbers to their parent context
let lines = textBody.split('\n').map(l => l.trim()).filter(l => l.length > 0);
let hasError = false;
const circleRegex = /[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/;
for (let line of lines) {
// Check if line is a branch marker header (contains symbol but doesn't start with a move number)
if (circleRegex.test(line) && !/^\s*\d+\./.test(line)) {
let match = line.match(circleRegex);
if (match) {
let marker = match[0];
// Jump back to the point indicated by the branch marker
if (branchMap[marker]) {
currNode = branchMap[marker].node;
currentFen = branchMap[marker].fen;
}
}
continue;
}
// Check if line contains moves
let moveMatch = line.match(/^\s*(\d+)\.\s*(.*)/);
if (moveMatch) {
let remainder = moveMatch[2];
let tokens = remainder.trim().split(/\s+/).filter(t => t.length > 0);
let parentContext = { node: currNode, fen: currentFen };
for (let token of tokens) {
if (/^[.]{3,}$|^…+$/.test(token)) {
continue; // Skip half-move ellipsis markers
}
// Check for inline branch marker
let circleMatch = token.match(circleRegex);
if (circleMatch) {
let marker = circleMatch[0];
// Map the marker to the state *before* this alternative move is made
branchMap[marker] = { node: parentContext.node, fen: parentContext.fen };
token = token.replace(circleRegex, '');
if (token === '') continue; // Skip if it was just the marker
}
let isRed = currentFen.includes(' w');
let moveEn = NotationConverter.toEnglish(token, isRed);
let dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
if (!dc) {
hasError = true;
break;
}
let nextFen = simulateMove(currentFen, dc);
// Append or select the child node
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;
}
// Update the parent context before shifting the cursor forward
parentContext = { node: currNode, fen: currentFen };
currNode = currNode.v[childIdx];
currentFen = nextFen;
}
} else {
// Regular text logic: append as a comment to the currently active node
if (currNode) {
if (currNode.c) currNode.c += '\n' + line;
else currNode.c = line;
}
}
}
if (hasError) {
alert("Parsed partially due to an invalid move notation.");
}
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;
}
// --- END OF NEW LOGIC ---
// 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
let movesArr = movesStr.replace(/\.\.\./g, ' ').split('\n');
for (let i = 0; i < movesArr.length; i++) {
if (/^\d+\./.test(movesArr[i].trim())) {
let j = i + 1;
while (j < movesArr.length) {
if (!/^\d+\./.test(movesArr[j].trim())) {
break;
}
j++;
}
movesStr = movesArr.slice(i, j).map(str => {
str = str.trim().replace(/^\d+\./g, '');
return str.trim().split(/\s+/).slice(0, 2).join(' ');
}).join('\n');
break;
}
}
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;
}
}
let currentFen = importedFen;
let vMap = buildVirtualMap(currentFen);
let currNode = 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;
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');
// Check for UCCI Engine Coordinate Format (e.g., "a0b0" or "h7e7")
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);
// Check if this move already exists as a branch from the current node
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 node is a fork (either it was already, or we just added a new branch), record the choice
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 && tokens.length > 0) {
alert("Parsed partially due to an invalid move notation.");
}
// If the final node we landed on (or existing path we merged into) has further branches, default to 0
while (currNode && currNode.v && currNode.v.length > 0) {
if (currNode.v.length > 1) {
currentBranch.push(0);
}
currNode = currNode.v[0];
}
currentStepIndex = attachIndex;
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
updateToolHighlights();
}