Chess Notation Export and URL Parameter

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.

1. Replace the renderExportTextUI function and add URL Hash helpers:

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);
           });
       }

2. Update importExportedText to support pasting URL Hashes:

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);
                   }
               }
           }

3. Update the "Boot the App" section to read the URL Hash:

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();
   });