Here are the modifications to support the "Address Parameter" (URL Hash) data format and to update the Export Text UI.
Replace the renderExportTextUI() function and add the URL encoding/decoding functions immediately above it. Also, update the "Boot the App" sequence at the end of the script to support parsing the URL hash on page load, and slightly update importExportedText() to detect pasted URLs.
Find the existing function renderExportTextUI() (around line 1445) and replace it entirely with the following code block, which includes the new encodeUrlParam, generateUrlHash, and parseUrlHash functions:
JavaScript
// --- URL Hash Generation & Parsing ---
function encodeUrlParam(node, isRoot) {
let res = "";
if (isRoot) {
if (node.fen === INITIAL_FEN) {
res += "i";
} else {
// Replace spaces with commas
res += node.fen.replace(/ /g, ',');
}
}
if (node.c) {
res += ";" + encodeURIComponent(node.c);
}
if (node.v && node.v.length > 0) {
if (node.v.length === 1) {
let child = node.v[0];
let moveStr = String.fromCharCode(97 + child.lastMove.startX) + (9 - child.lastMove.startY) +
String.fromCharCode(97 + child.lastMove.endX) + (9 - child.lastMove.endY);
res += "+" + moveStr + encodeUrlParam(child, false);
} else {
res += "@";
let branchStrs = [];
for (let i = 0; i < node.v.length; i++) {
let child = node.v[i];
let moveStr = String.fromCharCode(97 + child.lastMove.startX) + (9 - child.lastMove.startY) +
String.fromCharCode(97 + child.lastMove.endX) + (9 - child.lastMove.endY);
branchStrs.push(moveStr + encodeUrlParam(child, false));
}
res += branchStrs.join("&") + "$";
}
}
return res;
}
function generateUrlHash() {
let hashStr = encodeUrlParam(historyFEN, true);
// Remove trailing '$' characters
hashStr = hashStr.replace(/\$+$/, '');
return window.location.origin + window.location.pathname + "#" + hashStr;
}
function parseUrlHash(hashStr) {
let pos = 0;
function peek() { return pos < hashStr.length ? hashStr[pos] : null; }
function consume() { return pos < hashStr.length ? hashStr[pos++] : null; }
// 1. Read FEN
let fenStr = "";
if (peek() === 'i') {
consume();
fenStr = INITIAL_FEN;
} else {
let fenChars = [];
while(peek() !== null && peek() !== ';' && peek() !== '+' && peek() !== '@') {
fenChars.push(consume());
}
fenStr = fenChars.join('').replace(/,/g, ' '); // Restore spaces
}
let rootNode = { fen: fenStr, move: null, lastMove: null, c: "", v: [] };
function createChildFromMove(parentFen, moveStr) {
if (!moveStr || moveStr.length < 4) return null;
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);
let dc = { startX, startY, endX, endY };
let nextFen = simulateMove(parentFen, dc);
let vMap = buildVirtualMap(parentFen);
let pId = vMap.get(`${startX},${startY}`);
if (!pId) return null;
let moveNotation = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
return {
fen: nextFen,
move: moveNotation,
lastMove: dc,
c: "",
v: []
};
}
// 2. Recursively parse branches and comments
function parseNodeEnd(node) {
if (peek() === ';') {
consume();
let cChars = [];
while(peek() !== null && peek() !== '+' && peek() !== '@' && peek() !== '&' && peek() !== '$') {
cChars.push(consume());
}
node.c = decodeURIComponent(cChars.join(''));
}
if (peek() === '+') {
consume();
let m1 = consume(), m2 = consume(), m3 = consume(), m4 = consume();
let moveStr = (m1||'') + (m2||'') + (m3||'') + (m4||'');
let child = createChildFromMove(node.fen, moveStr);
if (child) {
node.v.push(child);
parseNodeEnd(child);
}
} else if (peek() === '@') {
consume();
while (true) {
let m1 = consume(), m2 = consume(), m3 = consume(), m4 = consume();
let moveStr = (m1||'') + (m2||'') + (m3||'') + (m4||'');
let child = createChildFromMove(node.fen, moveStr);
if (child) {
node.v.push(child);
parseNodeEnd(child);
}
if (peek() === '&') {
consume(); // Next branch
} else if (peek() === '$') {
consume(); // End of branches
break;
} else if (peek() === null) {
break; // EOF safely handled if trailing '$' is omitted
} else {
break;
}
}
}
}
parseNodeEnd(rootNode);
return rootNode;
}
// --- Refactored UI for Export Text ---
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);
const urlText = generateUrlHash();
// Generate UCCI Engine Format String
let path = getGamePath();
let engineText = "position fen " + path.fen;
let currNode = path;
while (currNode && currNode.v && currNode.v.length > 0) {
currNode = currNode.v[0];
let dc = currNode.lastMove;
if (dc) {
engineText += " moves " + String.fromCharCode(97 + dc.startX) + (9 - dc.startY) + String.fromCharCode(97 + dc.endX) + (9 - dc.endY);
}
}
// Create readonly textarea
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;" readonly></textarea>`;
const textarea = document.getElementById('export-textarea');
// Set up the 4 toggle buttons, distributed evenly vertically
commentDiv.innerHTML = `
<div style="display:flex; flex-direction:column; justify-content:space-evenly; align-items:center; height:100%; padding:10px; box-sizing:border-box;">
<div class="exp-fmt-btn active" data-fmt="cn" style="width:100%; padding:10px; text-align:center; background:#4CAF50; color:#fff; cursor:pointer; border-radius:4px; font-weight:bold;">Chinese Notation</div>
<div class="exp-fmt-btn" data-fmt="en" style="width:100%; padding:10px; text-align:center; background:#555; color:#fff; cursor:pointer; border-radius:4px; font-weight:bold;">English Notation</div>
<div class="exp-fmt-btn" data-fmt="engine" style="width:100%; padding:10px; text-align:center; background:#555; color:#fff; cursor:pointer; border-radius:4px; font-weight:bold;">Engine Format</div>
<div class="exp-fmt-btn" data-fmt="url" style="width:100%; padding:10px; text-align:center; background:#555; color:#fff; cursor:pointer; border-radius:4px; font-weight:bold;">URL Parameter</div>
</div>
`;
const formats = {
'cn': cnText,
'en': enText,
'engine': engineText,
'url': urlText
};
const formatBtns = commentDiv.querySelectorAll('.exp-fmt-btn');
formatBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Update active styling
formatBtns.forEach(b => {
b.style.background = '#555';
b.classList.remove('active');
});
btn.style.background = '#4CAF50';
btn.classList.add('active');
// Set value and select text
textarea.value = formats[btn.getAttribute('data-fmt')];
textarea.select();
});
});
// Initialize default state (Chinese)
textarea.value = cnText;
setTimeout(() => textarea.select(), 10);
btnDiv.innerHTML = `
<div class="ejceestextbtninner">
<div class="note-btn btn-confirm" id="exp-confirm" title="Confirm">
<svg viewBox="0 0 24 24"><path fill="#4CAF50" 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 fill="#F44336" 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 = textarea.value;
isExportTextMode = false;
importExportedText(text);
});
}
Find function importExportedText(text) (around line 1515) and insert this check immediately after the text = text.replace(/ r(\n|\s|$)/g, ' w$1'); normalizations:
JavaScript
// 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);
}
}
}
Scroll to the very bottom of the <script> tag. Replace the // --- Boot the App --- section with this code to process window.location.hash before falling back to localStorage:
JavaScript
// --- Boot the App ---
// 1. Check for URL Hash first, then fallback to localStorage
let initialHistory = null;
if (window.location.hash && window.location.hash.length > 1) {
try {
const hashStr = window.location.hash.substring(1);
const parsedTree = parseUrlHash(hashStr);
if (parsedTree && parsedTree.fen) {
initialHistory = {
historyFEN: parsedTree,
currentBranch: [],
currentStepIndex: 0,
moveInterval: 1.8,
moveSpeed: 0.3,
isRotateEnabled: false,
isFlipEnabled: false
};
// Rebuild branch arrays pointing to initial branch
let tempNode = parsedTree;
while (tempNode.v && tempNode.v.length) {
if (tempNode.v.length > 1) initialHistory.currentBranch.push(0);
tempNode = tempNode.v[0];
}
// Parse visual rotation/flip from FEN if available
let fenArr = parsedTree.fen.split(' ');
if (fenArr.length >= 4) {
initialHistory.isRotateEnabled = fenArr[2] === '1';
initialHistory.isFlipEnabled = fenArr[3] === '1';
}
}
} catch (e) {
console.error("Failed to parse URL hash:", e);
}
}
if (!initialHistory) {
const savedState = localStorage.getItem('ejcees_saved_state');
if (savedState) {
try {
initialHistory = JSON.parse(savedState);
} catch (e) {
console.error("Failed to parse saved state:", e);
}
}
}
// 2. Initialize with resolved state or default FEN
if (initialHistory && initialHistory.historyFEN) {
historyFEN = initialHistory.historyFEN;
currentBranch = initialHistory.currentBranch || [];
currentStepIndex = initialHistory.currentStepIndex || 0;
moveInterval = parseFloat(initialHistory.moveInterval) || moveInterval;
moveSpeed = parseFloat(initialHistory.moveSpeed) || moveSpeed;
isRotateEnabled = initialHistory.isRotateEnabled || false;
isFlipEnabled = initialHistory.isFlipEnabled || false;
loadFEN(historyFEN.fen);
} else {
loadFEN(INITIAL_FEN);
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
stepSlider.max = 0;
stepSlider.value = 0;
applyLastMoveVisuals(null);
highlightActiveStep(0);
}
// 3. Reset UI states based on loaded data
reapplyVisualPositions();
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
// 4. Initial save and UI update
saveStateToUndo();
updateToolHighlights();
});