Here are the modifications to address your requirements. All instructions and code comments are in English.
Update the .ejceesrcdstart class to add position: relative; and add the new .ejceesrcdstart.has-comment::after rule so it can display the comment icon exactly like .ejceesrcdstep.
Find this block in your <style> section:
CSS
.ejceesrcdstart {
width: 100%;
padding: 8px;
box-sizing: border-box;
text-align: center;
font-weight: bold;
border-bottom: 1px solid #444;
color: #aaa;
font-size: 14px;
cursor: pointer;
}
Replace it with the following:
CSS
.ejceesrcdstart {
width: 100%;
padding: 8px;
box-sizing: border-box;
text-align: center;
font-weight: bold;
border-bottom: 1px solid #444;
color: #aaa;
font-size: 14px;
cursor: pointer;
position: relative; /* Added for absolute positioning of the marker */
}
.ejceesrcdstart.has-comment::after {
content: '';
position: absolute;
bottom: 0;
right: 0;
width: 12px;
height: 12px;
background: url('data:image/svg+xml;utf8,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27><path fill=%27%23ffd700%27 d=%27M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z%27/></svg>') no-repeat center/contain;
pointer-events: none;
}
In the renderRecordUI() function, add logic to check if the root historyFEN has a comment and apply the class.
Find this part inside renderRecordUI():
JavaScript
if (historyFEN.fen === INITIAL_FEN) {
startDiv.textContent = '棋局開始,紅方先行';
} else if (historyFEN.fen.includes(' w')) {
startDiv.textContent = '特定局面,輪到紅方';
} else {
startDiv.textContent = '特定局面,輪到黑方';
}
recordContainer.innerHTML = '';
recordContainer.appendChild(startDiv);
Add the comment check right before appending:
JavaScript
if (historyFEN.fen === INITIAL_FEN) {
startDiv.textContent = '棋局開始,紅方先行';
} else if (historyFEN.fen.includes(' w')) {
startDiv.textContent = '特定局面,輪到紅方';
} else {
startDiv.textContent = '特定局面,輪到黑方';
}
// Check if root has comment
if (historyFEN.c && historyFEN.c.trim() !== '') {
startDiv.classList.add('has-comment');
}
recordContainer.innerHTML = '';
recordContainer.appendChild(startDiv);
We need getMoveNotation to accept a currentMap parameter to evaluate abstract logical states during text import without moving actual DOM elements.
Find the definition:
JavaScript
function getMoveNotation(pieceId, startX, startY, endX, endY, capturedId) {
Replace it with:
JavaScript
function getMoveNotation(pieceId, startX, startY, endX, endY, capturedId, currentMap = tileMap) {
Inside that function, find this loop:
JavaScript
for (const [pos, id] of tileMap.entries()) {
Replace it with:
JavaScript
for (const [pos, id] of currentMap.entries()) {
During SVG Export, record the ejceesrotate and ejceesflip attributes.
Inside the document.getElementById('tool-exp-svg').addEventListener('click', ...) callback, find this block:
JavaScript
// 2. Remove specific elements
if (isAnimation) {
Right before if (isAnimation) {, add these lines:
JavaScript
// Record current view states onto the SVG
cloneSvg.setAttribute('ejceesrotate', isRotateEnabled ? '1' : '0');
cloneSvg.setAttribute('ejceesflip', isFlipEnabled ? '1' : '0');
Then, update importExportedSVG to reverse these view states when parsing coordinates.
Find this function:
JavaScript
function importExportedSVG(svgText) {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgText, "image/svg+xml");
Modify the beginning of the function and coordinate extraction logic like so:
JavaScript
function importExportedSVG(svgText) {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgText, "image/svg+xml");
// Extract the saved view mode from the exported SVG
const isRotated = svgDoc.documentElement.getAttribute('ejceesrotate') === '1';
const isFlipped = svgDoc.documentElement.getAttribute('ejceesflip') === '1';
// Helper to reverse export-time visual coordinates back to logical coordinates
function parseExportVisCoords(px, py) {
let visX = Math.round((px - 24) / 48);
let visY = Math.round((py - 24) / 48);
let lX = visX; let lY = visY;
if (isRotated) { lX = 8 - lX; lY = 9 - lY; }
if (isFlipped) { lX = 8 - lX; }
return { x: lX, y: lY };
}
// 1. Get step0 for initial comment
let initialComment;
const step0 = svgDoc.getElementById('step0');
if (svgDoc.documentElement.hasAttribute('data-comment')) {
initialComment = b64DecodeUnicode(svgDoc.documentElement.getAttribute('data-comment'));
} else {
initialComment = step0 && step0.getAttribute('data-comment') ? b64DecodeUnicode(step0.getAttribute('data-comment')) : "";
}
// 2. Reconstruct Initial FEN from SVG DOM positions
const etboard = svgDoc.querySelector('.etboard');
const uses = etboard.querySelectorAll('use');
const boardMap = Array(10).fill(null).map(() => Array(9).fill(''));
let firstMovePieceId = null;
const step1 = svgDoc.getElementById('step1');
if (step1 && step1.parentElement) {
firstMovePieceId = step1.parentElement.getAttribute('id');
}
uses.forEach(use => {
const href = use.getAttribute('href') || use.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
const transform = use.getAttribute('transform');
const match = transform && transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
if (match) {
const px = parseFloat(match[1]);
const py = parseFloat(match[2]);
// Apply reversal for initial piece layout
const pt = parseExportVisCoords(px, py);
const x = pt.x;
const y = pt.y;
const id = use.getAttribute('id');
if (id && !id.endsWith('f') && x >= 0 && x < 9 && y >= 0 && y < 10) {
boardMap[y][x] = id[0];
}
}
}
});
let initialFenBoard = "";
for (let y = 0; y < 10; y++) {
let emptyCount = 0;
for (let x = 0; x < 9; x++) {
if (boardMap[y][x] === '') {
emptyCount++;
} else {
if (emptyCount > 0) {
initialFenBoard += emptyCount;
emptyCount = 0;
}
initialFenBoard += boardMap[y][x];
}
}
if (emptyCount > 0) initialFenBoard += emptyCount;
if (y < 9) initialFenBoard += "/";
}
let initialTurn = 'w';
if (firstMovePieceId && firstMovePieceId[0] === firstMovePieceId[0].toLowerCase()) {
initialTurn = 'b';
}
const baseFen = initialFenBoard + " " + initialTurn;
let newHistoryFEN = {
fen: baseFen,
move: null,
lastMove: null,
c: initialComment,
v: []
};
let currentNode = newHistoryFEN;
let currentFen = baseFen;
let stepIndex = 1;
loadFEN(baseFen);
// 3. Reconstruct moves by analyzing animateTransform values
while (true) {
const stepAnim = svgDoc.getElementById(`step${stepIndex}`);
if (!stepAnim) break;
const commentStr = stepAnim.getAttribute('data-comment');
const comment = commentStr ? b64DecodeUnicode(commentStr) : "";
const fromStr = stepAnim.getAttribute('from');
const toStr = stepAnim.getAttribute('to');
const pId = stepAnim.parentElement.getAttribute('id');
const fParts = fromStr.split(',');
const tParts = toStr.split(',');
// Apply reversal for animations
const startPt = parseExportVisCoords(parseFloat(fParts[0]), parseFloat(fParts[1]));
const endPt = parseExportVisCoords(parseFloat(tParts[0]), parseFloat(tParts[1]));
const startX = startPt.x;
const startY = startPt.y;
const endX = endPt.x;
const endY = endPt.y;
const capturedId = tileMap.get(`${endX},${endY}`) || null;
const moveNotation = getMoveNotation(pId, startX, startY, endX, endY, capturedId);
const dc = { startX, startY, endX, endY };
const nextFen = simulateMove(currentFen, dc);
tileMap.delete(`${startX},${startY}`);
tileMap.set(`${endX},${endY}`, pId);
let newNode = {
fen: nextFen,
move: moveNotation,
lastMove: dc,
c: comment,
v: []
};
currentNode.v.push(newNode);
currentNode = newNode;
currentFen = nextFen;
stepIndex++;
}
historyFEN = newHistoryFEN;
initBranch();
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
updateToolHighlights();
}
Update renderExportTextUI() to include the engine format logic and the toggle button SVG.
Find function renderExportTextUI() { and replace it entirely with:
JavaScript
function renderExportTextUI() {
const recordContainer = document.querySelector('.ejceesrecord');
const commentDiv = document.querySelector('.ejceescomment');
const btnDiv = document.querySelector('.ejceestextbtn');
const cnText = generateExportText(false);
const enText = generateExportText(true);
// Generate UCCI Engine Format String
let path = getGamePath();
let engineText = "position fen " + path.fen + " moves";
let currNode = path;
while(currNode && currNode.v && currNode.v.length > 0) {
currNode = currNode.v[0];
let dc = currNode.lastMove;
if(dc) {
// Convert to coordinates like a0b0
engineText += " " + String.fromCharCode(97 + dc.startX) + (9 - dc.startY) + String.fromCharCode(97 + dc.endX) + (9 - dc.endY);
}
}
let isEngineFormat = false;
recordContainer.innerHTML = `<textarea class="ejceescomment-edit" id="export-textarea" style="width:100%; height:100%; resize:none; border:none; outline:none; background:#2a2a2a; color:#fff; padding:8px; font-family:monospace; font-size:14px; white-space:pre-wrap; overflow:auto;">${cnText}</textarea>`;
const cnBlob = new Blob([cnText], { type: 'text/plain' });
const enBlob = new Blob([enText], { type: 'text/plain' });
const now = new Date();
const timestamp =
now.getFullYear().toString().padStart(4, '0') +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0');
// Insert Toggle Button alongside the links
commentDiv.innerHTML = `<div style="display:flex; flex-direction:column; gap:10px; padding:10px;">
<a href="${URL.createObjectURL(cnBlob)}" download="ejcees_fen_${timestamp}.txt" style="color:#66b2ff; text-decoration:none;">fen (${cnBlob.size} bytes)</a>
<a href="${URL.createObjectURL(enBlob)}" download="ejcees_en_fen_${timestamp}.txt" style="color:#66b2ff; text-decoration:none;">en_fen (${enBlob.size} bytes)</a>
<div id="exp-toggle" title="Toggle Format" style="width: 72px; height: 48px; border-radius: 4px; display: flex; justify-content: center; align-items: center; cursor: pointer; background-color: #555; margin-top: 10px;">
<svg viewBox="0 0 24 24" style="width:32px;height:32px;fill:#fff;"><path d="M12 4l-4 4h3v7h2V8h3l-4-4zm0 16l4-4h-3V9h-2v7H8l4 4z"/></svg>
</div>
</div>`;
btnDiv.innerHTML = `
<div class="ejceestextbtninner">
<div class="note-btn btn-confirm" id="exp-confirm" title="Confirm">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
<div class="note-btn btn-cancel" id="exp-cancel" title="Cancel">
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</div>
</div>
`;
document.getElementById('exp-cancel').addEventListener('click', () => {
isExportTextMode = false;
renderRecordUI();
renderNoteUI();
updateToolHighlights();
});
document.getElementById('exp-confirm').addEventListener('click', () => {
const text = document.getElementById('export-textarea').value;
isExportTextMode = false;
importExportedText(text);
});
document.getElementById('exp-toggle').addEventListener('click', () => {
isEngineFormat = !isEngineFormat;
document.getElementById('export-textarea').value = isEngineFormat ? engineText : cnText;
});
}
Add a helper function to create logic maps during FEN manipulation, and rewrite importExportedText() to fulfill the branch matching/attachment rules and engine format parsing.
Find function importExportedText(text) { and replace it and its contents entirely with:
JavaScript
// Helper to evaluate notations without touching the actual DOM layout
function buildVirtualMap(fen) {
const map = new Map();
const counts = {};
const rows = fen.split(' ')[0].split('/');
rows.forEach((row, y) => {
let x = 0;
for (let i = 0; i < row.length; i++) {
const char = row[i];
if (/[0-9]/.test(char)) x += parseInt(char, 10);
else {
counts[char] = counts[char] || 0;
map.set(`${x},${y}`, `${char}${counts[char]++}`);
x++;
}
}
});
return map;
}
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 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);
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 fenMatch = text.match(/(?:position fen )?([a-zA-Z0-9/]+ [wb])/);
let movesStr = text;
if (fenMatch) {
importedFen = fenMatch[1];
// Strip the FEN string declaration so it isn't parsed as moves later
movesStr = movesStr.replace(/position fen [a-zA-Z0-9/]+ [wb](\s+moves)?/, '');
if (movesStr === text) {
movesStr = movesStr.replace(fenMatch[1], '');
}
}
// 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
// Remove line numbers (e.g., "1. " or " 23. ") and ellipsis "..."
movesStr = movesStr.replace(/^\d+\.\s*/gm, ' ').replace(/\s\d+\.\s/g, ' ').replace(/\.\.\./g, ' ');
let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
let currentFen = importedFen;
let vMap = buildVirtualMap(currentFen);
let currNode = attachNode;
let attachDepth = attachIndex;
// Truncate currentBranch up to the attachIndex to prepare for the new branch selection
currentBranch.length = attachDepth;
let hasError = false;
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
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; }
// Generate UI notation dynamically from coordinates
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
// Standard English or Chinese notation
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) {
// Create new branch
let newNode = { fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] };
currNode.v.push(newNode);
childIdx = currNode.v.length - 1;
}
// Force selection onto this path
currentBranch[attachDepth] = childIdx;
currNode = currNode.v[childIdx];
currentFen = nextFen;
attachDepth++;
// Update virtual map for next iterations
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.");
}
// Pad currentBranch with 0s if the newly attached path has existing single children further down
while (currNode && currNode.v && currNode.v.length > 0) {
currentBranch[attachDepth] = 0;
currNode = currNode.v[0];
attachDepth++;
}
// Jump to the parent node of the newly added branch (attach point)
currentStepIndex = attachIndex;
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
updateToolHighlights();
}