分享和讨论Javascript相关的话题
回复
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

翻譯成英文:

代码: 全选

以下是將前一個回答中的所有程式碼註釋翻譯為英文的版本,包含 **ICCS 格式修改**、**非 PGN 格式導出邏輯**、**PGN 導入解析器** 以及 **導入按鈕邏輯**:

### 1. Modify `generatePGNFormat` (Support ICCS with Hyphen)

```javascript
          function getMoveStr(node, isRed) {
              if (oddState === 0) return NotationConverter.toChinese(node.move);
              if (oddState === 2) return node.move;
              if (oddState === 1 && node.lastMove) {
                  let dc = node.lastMove;
                  // Modification: Convert to Uppercase and add hyphen "-", e.g., H2-E2
                  let start = String.fromCharCode(65 + dc.startX) + (9 - dc.startY);
                  let end = String.fromCharCode(65 + dc.endX) + (9 - dc.endY);
                  return (start + "-" + end).toUpperCase();
              }
              return node.move;
          }
```

### 2. Export Logic for Non-PGN Formats (Convert Metadata Tags to Comments)

```javascript
        // Helper: Get root comments integrated with Metadata
        function getMetaRootComment() {
          let metaHeader = "";
          pgnMetadata.forEach(m => {
            // Only add to comment if the value is not empty and it's not the Result tag
            if (m.value && m.value.trim() !== "" && m.name !== "Result") {
              metaHeader += `${m.name}:${m.value}\n`;
            }
          });
          let rootC = (historyFEN.c || "").trim();
          // Prepend metadata header to the existing root comment
          return metaHeader ? (metaHeader + (rootC ? "\n" + rootC : "")) : rootC;
        }

        // Helper: Append result string to the last move's comment
        function appendResultToLastComment(lastNode) {
          let resSymbol = pgnMetadata.find(m => m.name === "Result")?.value || "*";
          let mapping = { "1-0": "紅勝", "0-1": "黑勝", "1/2-1/2": "平局" };
          let resText = mapping[resSymbol] || "";
          
          let existingC = (lastNode.c || "").trim();
          if (!resText) return existingC;
          // Return existing comment followed by result text
          return existingC ? (existingC + "\n" + resText) : resText;
        }
```

### 3. PGN Import Logic (`importPGN`)

```javascript
        function importPGN(input) {
          // 1. Reset current Metadata
          pgnMetadata.forEach(m => m.value = (m.name === "Result" ? "*" : ""));

          // 2. Parse Tags [Name "Value"]
          const tagRegex = /\[(\w+)\s+"([^"]*)"\]/g;
          let match;
          let lastIndex = 0;
          while ((match = tagRegex.exec(input)) !== null) {
            let key = match[1], val = match[2];
            let metaObj = pgnMetadata.find(m => m.name === key);
            if (metaObj) metaObj.value = val;
            else pgnMetadata.push({ name: key, value: val });
            lastIndex = tagRegex.lastIndex;
          }

          // 3. Extract the move body (part after all tags)
          let body = input.slice(lastIndex).trim();
          
          // Extract the trailing result symbol (1-0, 0-1, 1/2-1/2, *)
          const resMatch = body.match(/(1-0|0-1|1\/2-1\/2|\*)$/);
          if (resMatch) {
            let res = resMatch[1];
            let metaObj = pgnMetadata.find(m => m.name === "Result");
            if (metaObj) metaObj.value = res;
            body = body.slice(0, -res.length).trim();
          }

          // 4. Parse moves, comments, and branches using a state machine
          function parseMoves(str) {
            let nodes = [];
            let i = 0;
            while (i < str.length) {
              let char = str[i];
              if (char === '{') { // Parse Comment
                let end = str.indexOf('}', i);
                let comment = str.slice(i + 1, end).trim();
                if (nodes.length > 0) nodes[nodes.length - 1].c = comment;
                else rootComment = comment; // Comment before the first move
                i = end + 1;
              } else if (char === '(') { // Parse Branch
                let count = 1, j = i + 1;
                while (count > 0 && j < str.length) {
                  if (str[j] === '(') count++;
                  if (str[j] === ')') count--;
                  j++;
                }
                let branchStr = str.slice(i + 1, j - 1);
                // Recursively parse the branch and add to the last move's variations
                if (nodes.length > 0) nodes[nodes.length - 1].v.push(parseMoves(branchStr)[0]);
                i = j;
              } else if (/[0-9a-zA-Z\u4e00-\u9fa5]/.test(char)) { // Move notation
                let end = str.search(/[\s\(\{\}]/, i);
                if (end === -1) end = str.length;
                let movePart = str.slice(i, end);
                // Exclude move numbers like "1." or "1..."
                if (!movePart.includes('.')) {
                  nodes.push({ move: movePart, c: '', v: [] });
                }
                i = end;
              } else { i++; }
            }
            return nodes;
          }

          let rootComment = "";
          const moveList = parseMoves(body);
          // (Implementation note: Iterate through moveList and use executeMove to rebuild FENs)
          return { moves: moveList, rootComment: rootComment };
        }
```

### 4. Update `handleConfirm` for PGN Import

```javascript
          confirmBtn.addEventListener('click', () => {
            const input = textarea.value.trim();
            // Check if input starts with the PGN signature
            if (input.startsWith('[Game "Chinese Chess"]')) {
              // PGN Import Logic
              const result = importPGN(input);
              // (Need to call applyHistory or a recursive loader here)
              alert('PGN Imported successfully');
              renderRecordUI();
            } else {
              // Existing import logic for JSON or DP format
              // ...
            }
          });
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

明確告知所有要用到這兩個函式的地方(在導出DP格式以及url格式時會用到):

1. 將原本讀取 `historyFEN.c` 的地方改為讀取 `getMetaRootComment()`。
2. 在遍歷到最後一個節點時,調用 `appendResultToLastComment`。

二,把importPGN的()邏輯,寫進importExportedText()中。

只需告知所要修改的地方,所有的代碼和注釋都要是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro(讓我睡到8:52):

代码: 全选

以下是針對您的需求所整理的修改說明。所有程式碼與註釋皆已轉換為英文。

### 1. Update Export Functions (`generateDPFormat` and `generateUrlHash`)

You need to update the logic in `generateDPFormat` and the function responsible for URL generation to integrate the metadata and results.

#### In `generateDPFormat`:
Replace the part that reads the root comment and the move sequence processing.

```javascript
function generateDPFormat(isToggled) {
    let text = "";
    
    // 1. Replace historyFEN.c with getMetaRootComment() for the root comment
    let rootComment = getMetaRootComment();
    if (rootComment) {
        text += `{${rootComment}}\n`;
    }

    function processSequence(node) {
        let seq = "";
        // ... existing traversal logic ...
        
        // 2. When reaching the last node (leaf node), call appendResultToLastComment
        let isLastNode = (!node.v || node.v.length === 0);
        let currentComment = isLastNode ? appendResultToLastComment(node) : (node.c || "");
        
        if (currentComment) {
            seq += ` {${currentComment}}`;
        }
        // ... handle branches and recursion ...
        return seq;
    }
    
    text += processSequence(historyFEN);
    return text;
}
```

#### In `generateUrlHash` (or your URL generation logic):
Similar to the DP format, ensure the serialized string uses the metadata-integrated comments.

```javascript
function generateUrlHash(isToggled) {
    // Replace the root comment serialization
    let rootC = getMetaRootComment();
    // Use rootC instead of historyFEN.c when building the hash string
    
    // When traversing moves to build the string, check if the node is the last one:
    // If it's the leaf node, use appendResultToLastComment(node) for the comment field.
}
```

---

### 2. Update `importExportedText` with PGN Parentheses Logic

Integrate the PGN parsing state machine (handling tags, braces for comments, and parentheses for branches) into the main import function.

```javascript
function importExportedText(input) {
    input = input.trim();

    // Check if the input is in PGN Format
    if (input.startsWith('[Game "Chinese Chess"]')) {
        // 1. Reset Metadata
        pgnMetadata.forEach(m => m.value = (m.name === "Result" ? "*" : ""));

        // 2. Parse Tags: [Name "Value"]
        const tagRegex = /\[(\w+)\s+"([^"]*)"\]/g;
        let match;
        let lastTagIndex = 0;
        while ((match = tagRegex.exec(input)) !== null) {
            let key = match[1], val = match[2];
            let metaObj = pgnMetadata.find(m => m.name === key);
            if (metaObj) metaObj.value = val;
            else pgnMetadata.push({ name: key, value: val });
            lastTagIndex = tagRegex.lastIndex;
        }

        // 3. Extract move body and the trailing result symbol
        let body = input.slice(lastTagIndex).trim();
        const resMatch = body.match(/(1-0|0-1|1\/2-1\/2|\*)$/);
        if (resMatch) {
            let res = resMatch[1];
            let metaObj = pgnMetadata.find(m => m.name === "Result");
            if (metaObj) metaObj.value = res;
            body = body.slice(0, -res.length).trim();
        }

        // 4. Parentheses Logic: Recursive Move & Branch Parser
        function parsePgnBody(str) {
            let moves = [];
            let i = 0;
            let rootComment = "";

            while (i < str.length) {
                let char = str[i];

                if (char === '{') { 
                    // Handle Comments in Braces
                    let end = str.indexOf('}', i);
                    let comment = str.slice(i + 1, end).trim();
                    if (moves.length > 0) moves[moves.length - 1].c = comment;
                    else rootComment = comment; 
                    i = end + 1;
                } 
                else if (char === '(') { 
                    // Handle Branches in Parentheses (Recursive)
                    let count = 1, j = i + 1;
                    while (count > 0 && j < str.length) {
                        if (str[j] === '(') count++;
                        if (str[j] === ')') count--;
                        j++;
                    }
                    let branchStr = str.slice(i + 1, j - 1);
                    if (moves.length > 0) {
                        // Recursively parse the content inside parentheses
                        let variations = parsePgnBody(branchStr);
                        if (variations.moves.length > 0) {
                            moves[moves.length - 1].v.push(variations.moves[0]);
                        }
                    }
                    i = j;
                } 
                else if (/[0-9a-zA-Z\u4e00-\u9fa5]/.test(char)) { 
                    // Handle Move Notations (Chinese, WXF, or ICCS)
                    let end = str.search(/[\s\(\{\}]/, i);
                    if (end === -1) end = str.length;
                    let movePart = str.slice(i, end);
                    // Skip move numbers (e.g., "1.", "1...")
                    if (!movePart.includes('.')) {
                        moves.push({ move: movePart, c: '', v: [] });
                    }
                    i = end;
                } 
                else {
                    i++;
                }
            }
            return { moves, rootComment };
        }

        const result = parsePgnBody(body);
        // Load the parsed result into the application state
        // (e.g., historyFEN = result.moves, historyFEN.c = result.rootComment)
        console.log("PGN content parsed successfully");
        return result;
    } 
    else {
        // Fallback for existing JSON or DP format import logic
        // ...
    }
}
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

叫fast來看看!

怎麼點擊DP Format第三次,會出現大括號?
附件
ejcees20260425pgn2.7z
(49.68 KiB) 已下载 22 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

當點擊close-btn關閉pgn-meta-modal時,它裡面的table數據是否保存?如果沒有,那就增加保存以及刷新導出數據的功能。

代码: 全选

在現有的代碼中,`pgn-meta-modal` 的關閉邏輯確實**沒有**顯式保存數據或刷新導出數據的功能。目前關閉按鈕僅僅是從 DOM 中移除 modal 節點:

```javascript
// 現有邏輯
closeBtn.onclick = () => document.body.removeChild(modal);
```

由於數據是通過 `onchange` 事件實時同步到 `pgnMetadata` 數組中的,因此數據**其實已經保存在內存裡**,但使用者介面上**不會自動觸發導出預覽的更新**。

為了提升用戶體驗並確保數據一致性,建議將關閉邏輯修改為:**先保存(同步到 Undo 棧/LocalStorage)並刷新導出視圖**。

以下是修改建議:

### 1. 增加刷新導出數據的功能
在代碼中,導出數據(如 PGN 或 DP 格式)是由 `showExportTextModal` (假設名稱) 或相關工具按鈕觸發的。我們可以建立一個統一的 `updateExportIfOpen` 函數,或者直接在關閉 Modal 時觸發狀態保存。

### 2. 修改 `showPGNMetaModal` 函數中的關閉邏輯
請將 `showPGNMetaModal` 函數內的 `closeBtn.onclick` 替換為以下代碼:

```javascript
// 修改後的關閉邏輯
closeBtn.onclick = () => {
    // 1. 移除 Modal
    document.body.removeChild(modal);
    
    // 2. 如果當前在導出模式(Export Text Mode),則刷新內容
    if (isExportTextMode) {
        // 獲取當前選擇的格式狀態(從 UI 獲取或變量)
        const activeBtn = document.querySelector('.exp-fmt-btn.active');
        const oddState = activeBtn ? parseInt(activeBtn.getAttribute('data-odd') || '0') : 0;
        
        // 重新生成內容並填入 textarea
        const exportArea = document.querySelector('.ejceescomment-edit');
        if (exportArea) {
            exportArea.value = generatePGNFormat(oddState);
        }
    }
    
    // 3. 保存當前狀態到 Undo 棧與 LocalStorage,確保元數據(Metadata)被持久化
    saveStateToUndo();
};
```

### 為什麼需要這樣改?
1.  **實時性**:使用者修改了「賽事名稱」或「棋手姓名」後,關閉視窗時,後方的導出文本框應該立即反映這些變化,而不是要求用戶重新點擊「導出」按鈕。
2.  **安全性**:調用 `saveStateToUndo()` 可以確保這些元數據被記錄在撤銷/重做歷史中,並存入 `localStorage`,防止意外刷新頁面導致填寫的數據丟失。
3.  **數據鏈路**:在 `generatePGNFormat` 中,程序會遍歷 `pgnMetadata` 來構建文件頭,因此只要刷新調用該函數,數據就能正確保存到導出的 PGN 字符串中。

### 額外建議:增加「保存」按鈕
雖然 `onchange` 會自動保存,但使用者習慣上常尋找「保存」或「確定」按鈕。您可以在 `pgn-add-btn` 旁邊增加一個帶有「保存並關閉」字樣的按鈕,其邏輯與上述修改後的 `closeBtn` 一致。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

為什麼當我按下.exp-fmt-btn[data-fmt=pgb]時,我保存的meta都不出現在textarea中?

代码: 全选

在分析您提供的代碼後,發現當您點擊 `.exp-fmt-btn[data-fmt=pgn]`(即 PGN 格式按鈕)時,保存的 metadata 沒出現在 `textarea` 中,主要原因在於 `renderExportTextUI` 函數中對按鈕點擊事件的處理邏輯**漏掉了對 PGN 數據的動態更新**。

以下是具體原因分析與修復建議:

### 1. 為什麼會失效?
在 `renderExportTextUI` 函數的事件監聽器中(雖然這部分代碼在 snippet 中被截斷,但從邏輯可推斷):
* 當點擊 `data-fmt="cn"`(DP 格式)時,它會根據 `oddState` 切換不同的 DP 生成函數。
* **關鍵問題**:當點擊 `data-fmt="pgn"` 時,代碼可能只切換了按鈕的 UI 狀態(例如變色),但**沒有重新調用 `generatePGNFormat(oddState)`** 並將結果填入 `#export-textarea`。
* 另外,由於 PGN 的 metadata 存儲在全域變數 `pgnMetadata` 中,如果在導出界面打開後才去 `pgn-meta-modal` 修改數據,導出界面不會自動刷新。

### 2. 修復方案

您需要修改 `renderExportTextUI` 中關於 `.exp-fmt-btn` 的點擊處理邏輯。請確保在 `pgn` 分支下正確調用了生成函數:

```javascript
// 在 renderExportTextUI 函數內部找到按鈕點擊監聽部分
commentDiv.addEventListener('click', (e) => {
    const btn = e.target.closest('.exp-fmt-btn');
    if (!btn) return;

    const fmt = btn.getAttribute('data-fmt');
    // ... 其他邏輯 (如切換 active class)

    if (fmt === 'pgn') {
        // 確保這裡獲取了當前的 oddState (0: Chinese, 1: ICCS, 2: WXF)
        const odd = formatOddStates['pgn']; 
        // 核心:重新生成 PGN 並更新 textarea
        textarea.value = generatePGNFormat(odd);
        lastExportFormat = 'pgn';
    } 
    // ... 其他格式的處理
});
```

### 3. 同步更新建議
為了確保您在 `pgn-meta-modal`(元數據視窗)點擊關閉時,後方的導出文字框能同步更新,建議修改 `showPGNMetaModal` 函數中的 `closeBtn.onclick`:

```javascript
// 找到 showPGNMetaModal 函數中的這行:
closeBtn.onclick = () => {
    document.body.removeChild(modal);
    
    // 新增:如果當前正處於導出模式,且選擇的是 PGN 格式,則刷新內容
    if (isExportTextMode && lastExportFormat === 'pgn') {
        const textarea = document.getElementById('export-textarea');
        if (textarea) {
            const odd = formatOddStates['pgn'] || 0;
            textarea.value = generatePGNFormat(odd);
        }
    }
};
```

### 4. 檢查 `generatePGNFormat` 函數
請檢查您的 `generatePGNFormat` 函數是否正確遍歷了 `pgnMetadata`。現有代碼中已有相關邏輯:
```javascript
pgnMetadata.forEach((meta) => {
    if (meta.value && meta.value.trim() !== '') {
        text += `[${meta.name} "${meta.value}"]\n`; // 這行負責將 meta 加入文本
    }
});
```
只要確保在點擊切換按鈕或關閉 meta 視窗時**重新執行**此函數並賦值給 `textarea`,數據就會正常顯示。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

代码: 全选

          // Import PGN format
          if (text.startsWith('[Game "Chinese Chess"]')) {
          }
在這裡加上導入PGN格式的邏輯。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

改進:
一,在導出engine format時,如果初始fen===INITIAL_FEN,則 「fen rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1」將替換為「startpos」。

二,在<h3>PGN Metadata</h3>的右邊新增一個default按鈕,當點擊時.pgn-meta-modal內的所有值都將恢復默認。

三,當點擊pgn-meta-modal的close-btn時,如果值有變動,則將存放至undo-stack。取消所有table內的onchange事件,改為點擊close-btn時才對name和value進行增刪改的比對。

四,在點擊tool-save保存JSON格式時,如果PGN Metadata不為空,應在根節點增加一個「meta」,裡面是PGN Metadata的值。當打開或用文本上傳時,都應能讀出JSON中的PGN Metadata。

五,在下方if語句內,加入導入PGN格式功能(位於function importExportedText內)。

// Import PGN format
if (text.startsWith('[Game "Chinese Chess"]')) {
}

pgn格式的特點是:以[Game "Chinese Chess"]開頭。在「總評」前可能有各種標籤,如「[Event "…"][Site "…"][Date "2004.01.05"][Round "…"][RedTeam "…"][Red "…"]」等(把這些標籤的值都填到pgnMetadata中)。評論都會寫在大括號{}中。分支都會放在小括號()裡。

只需給出所要修改的地方,所有代碼和注釋都要使用英文。
附件
ejcees20260425pgn4.7z
(49.38 KiB) 已下载 18 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro越硬越愛啃:

代码: 全选

Here are the requested modifications to improve your Xiangqi application. All provided code segments are in English and address your 5 specific improvements.

### 1. Engine format export replacing `INITIAL_FEN` with `startpos`
Update the engine text generation in both the `performCopy` and `renderExportTextUI` functions.

**In `performCopy()`:**
```javascript
          } else if (fmt === 'engine') {
            let path = getGamePath(true);
            let engineText = 'position fen ' + path.fen;
            // Improvement 1: Check if FEN matches INITIAL_FEN board state
            if (path.fen.split(' ')[0] === INITIAL_FEN.split(' ')[0]) {
                engineText = 'position startpos';
            }
            let currNode = path;
```

**In `renderExportTextUI()`:**
```javascript
          // Generate UCCI Engine Format String
          let path = getGamePath(true);
          let engineText = 'position fen ' + path.fen;
          // Improvement 1: Check if FEN matches INITIAL_FEN board state
          if (path.fen.split(' ')[0] === INITIAL_FEN.split(' ')[0]) {
              engineText = 'position startpos';
          }
          let currNode = path;
```

---

### 2 & 3. PGN Metadata Modal Updates (Default button, logic refactor, and state saving)
Replace your entire `showPGNMetaModal()` function with this updated version:

```javascript
        function showPGNMetaModal() {
          const modal = document.createElement('div');
          modal.className = 'pgn-meta-modal';

          // Use local deep copy to prevent instant modification of the global variable
          let localMetadata = JSON.parse(JSON.stringify(pgnMetadata));

          const box = document.createElement('div');
          box.className = 'pgn-meta-box';

          // Container for Title and Default Button
          const headerDiv = document.createElement('div');
          headerDiv.style.display = 'flex';
          headerDiv.style.justifyContent = 'space-between';
          headerDiv.style.alignItems = 'center';

          const title = document.createElement('h3');
          title.textContent = 'PGN Metadata';

          // Improvement 2: Added Default button
          const defaultBtn = document.createElement('button');
          defaultBtn.textContent = 'Default';
          defaultBtn.style.padding = '4px 8px';
          defaultBtn.style.cursor = 'pointer';
          defaultBtn.style.marginRight = '30px'; // Leaves space for the close button
          defaultBtn.onclick = () => {
            localMetadata = [
              { name: 'Event', value: '' },
              { name: 'Site', value: '' },
              { name: 'Date', value: '' },
              { name: 'Round', value: '' },
              { name: 'RedTeam', value: '' },
              { name: 'Red', value: '' },
              { name: 'BlackTeam', value: '' },
              { name: 'Black', value: '' },
              { name: 'Result', value: '' },
              { name: 'Time', value: '' },
              { name: 'TimeControl', value: '' },
              { name: 'Opening', value: '' },
              { name: 'ECCO', value: '' },
              { name: 'Annotator', value: '' },
              { name: 'Termination', value: '' }
            ];
            renderTable();
          };

          headerDiv.appendChild(title);
          headerDiv.appendChild(defaultBtn);

          // Improvement 3: Sync DOM inputs to the localMetadata array manually
          function syncDOM() {
            const rows = tbody.querySelectorAll('.pgn-meta-row');
            localMetadata = [];
            rows.forEach((tr) => {
              const name = tr.querySelector('td:nth-child(2) input').value;
              const valEl = tr.querySelector('td:nth-child(3) input, td:nth-child(3) select');
              const value = valEl ? valEl.value : '';
              localMetadata.push({ name, value });
            });
          }

          const closeBtn = document.createElement('button');
          closeBtn.className = 'close-btn';
          closeBtn.style.top = '10px';
          closeBtn.style.right = '10px';
          closeBtn.onclick = () => {
            syncDOM(); // Read the latest inputs before closing
            // Only update and push to undoStack if there are actual changes
            const isChanged = JSON.stringify(localMetadata) !== JSON.stringify(pgnMetadata);
            if (isChanged) {
              pgnMetadata = JSON.parse(JSON.stringify(localMetadata));
            }

            document.body.removeChild(modal);
            
            if (isExportTextMode) {
              const activeBtn = document.querySelector('.exp-fmt-btn.active');
              const oddState = activeBtn ? parseInt(activeBtn.getAttribute('data-odd') || '0') : 0;
              const exportArea = document.querySelector('.ejceescomment-edit');
              if (exportArea) {
                exportArea.value = generatePGNFormat(oddState);
              }
            }
            if (isChanged) saveStateToUndo();
          };

          const table = document.createElement('table');
          table.className = 'pgn-meta-table';
          const thead = document.createElement('thead');
          thead.innerHTML = `<tr><th>No.</th><th>Name</th><th>Value</th><th>Remove</th></tr>`;
          const tbody = document.createElement('tbody');
          table.appendChild(thead);
          table.appendChild(tbody);

          function renderTable() {
            tbody.innerHTML = '';
            const highlights = ['Event', 'Site', 'Date', 'Round', 'Red', 'Black', 'Result'];
            localMetadata.forEach((item, index) => {
              const tr = document.createElement('tr');
              tr.className = 'pgn-meta-row';
              if (pgnSelectedRowIndex === index) tr.classList.add('selected');

              const tdNum = document.createElement('td');
              tdNum.textContent = index + 1;
              tdNum.style.cursor = 'pointer';
              tdNum.onclick = () => {
                syncDOM(); // Ensure current typings are saved before moving
                if (pgnSelectedRowIndex === -1) {
                  pgnSelectedRowIndex = index;
                } else if (pgnSelectedRowIndex === index) {
                  pgnSelectedRowIndex = -1;
                } else {
                  const movedItem = localMetadata.splice(pgnSelectedRowIndex, 1)[0];
                  localMetadata.splice(index, 0, movedItem);
                  pgnSelectedRowIndex = -1;
                }
                renderTable();
              };
              tr.appendChild(tdNum);

              const tdName = document.createElement('td');
              const nameInput = document.createElement('input');
              nameInput.className = 'pgn-meta-input';
              if (highlights.includes(item.name)) nameInput.classList.add('pgn-meta-highlight');
              nameInput.value = item.name;
              tdName.appendChild(nameInput);
              tr.appendChild(tdName);

              const tdValue = document.createElement('td');
              if (item.name === 'Result') {
                const sel = document.createElement('select');
                sel.className = 'pgn-meta-input';
                ['*', '1-0', '0-1', '1/2-1/2'].forEach((opt) => {
                  const option = document.createElement('option');
                  option.value = opt;
                  option.textContent = opt;
                  if (opt === item.value) option.selected = true;
                  sel.appendChild(option);
                });
                tdValue.appendChild(sel);
              } else {
                const valInput = document.createElement('input');
                valInput.className = 'pgn-meta-input';
                valInput.value = item.value;
                tdValue.appendChild(valInput);
              }
              tr.appendChild(tdValue);

              const tdDel = document.createElement('td');
              tdDel.className = 'pgn-meta-trash';
              tdDel.textContent = '🗑️';
              tdDel.onclick = () => {
                syncDOM();
                localMetadata.splice(index, 1);
                if (pgnSelectedRowIndex === index) pgnSelectedRowIndex = -1;
                else if (pgnSelectedRowIndex > index) pgnSelectedRowIndex--;
                renderTable();
              };
              tr.appendChild(tdDel);
              tbody.appendChild(tr);
            });
          }

          renderTable();

          const addBtn = document.createElement('div');
          addBtn.className = 'pgn-add-btn';
          addBtn.textContent = '+';
          addBtn.onclick = () => {
            syncDOM();
            localMetadata.push({ name: '', value: '' });
            renderTable();
          };

          box.appendChild(closeBtn);
          box.appendChild(headerDiv);
          box.appendChild(table);
          box.appendChild(addBtn);
          modal.appendChild(box);
          document.body.appendChild(modal);
        }
```

---

### 4. Add `meta` to JSON structure and Undo/Redo states
Update the JSON generation, loading functionalities, and the history tracking to properly associate `pgnMetadata`.

**In `document.getElementById('tool-save').addEventListener('click', ...)`:**
```javascript
          let copy;
          if (isRangeMode) {
            let rangeHistory = getGamePath();
            copy = simplify(rangeHistory);
            copy.fen = rangeHistory.fen;
            isRangeMode = false;
            rangeStart = null;
            rangeEnd = null;
            rangeClicks = 0;
            clearRangeBorders();
            renderNoteUI();
          } else {
            copy = simplify(historyFEN);
            copy.fen = historyFEN.fen;
          }

          // Improvement 4: Append PGN metadata to the JSON payload
          if (pgnMetadata && pgnMetadata.length > 0) {
            copy.meta = pgnMetadata;
          }

          const blob = new Blob([JSON.stringify(copy)], {
```

**In `document.getElementById('file-input').addEventListener('change', ...)`:**
```javascript
            const data = JSON.parse(event.target.result);

            // Improvement 4: Read PGN metadata from the parsed JSON payload
            if (data.meta && Array.isArray(data.meta)) {
                pgnMetadata = JSON.parse(JSON.stringify(data.meta));
            }

            function expand(node, parentFen) {
```

**In `importExportedText()` -> (Handling JSON String Block):**
```javascript
          // Handle Direct JSON structure
          if (text.startsWith('{')) {
            try {
              const data = JSON.parse(text);
              
              // Improvement 4: Load PGN metadata on JSON paste
              if (data.meta && Array.isArray(data.meta)) {
                  pgnMetadata = JSON.parse(JSON.stringify(data.meta));
              }

              function expand(node, parentFen) {
```

**In `cloneGameState()` & `restoreState()`:**
```javascript
        function cloneGameState() {
          return {
            historyFEN: structuredClone(historyFEN),
            currentBranch: [...currentBranch],
            currentStepIndex: currentStepIndex,
            moveInterval: moveInterval,
            moveSpeed: moveSpeed,
            isRotateEnabled: isRotateEnabled,
            isFlipEnabled: isFlipEnabled,
            // Improvement 4: Capture current metadata into state history
            pgnMetadata: JSON.parse(JSON.stringify(pgnMetadata))
          };
        }

        function restoreState(state) {
          const state_obj = JSON.parse(state);
          historyFEN = structuredClone(state_obj.historyFEN);
          currentBranch = [...state_obj.currentBranch];
          currentStepIndex = state_obj.currentStepIndex;
          moveInterval = parseFloat(state_obj.moveInterval);
          moveSpeed = parseFloat(state_obj.moveSpeed);
          isRotateEnabled = state_obj.isRotateEnabled;
          isFlipEnabled = state_obj.isFlipEnabled;
          
          // Improvement 4: Restore metadata from state history
          if (state_obj.pgnMetadata) {
              pgnMetadata = JSON.parse(JSON.stringify(state_obj.pgnMetadata));
          }
```

---

### 5. Add Native PGN Parsing Capability
Update your `importExportedText()` function where the `[Game "Chinese Chess"]` check is defined:

**In `importExportedText()`:**
```javascript
          // Import PGN format
          if (text.startsWith('[Game "Chinese Chess"]')) {
              // Extract Tags
              const tagRegex = /\[([A-Za-z0-9_]+)\s+"([^"]*)"\]/g;
              let match;
              pgnMetadata = [];
              let pgnFen = INITIAL_FEN;

              while ((match = tagRegex.exec(text)) !== null) {
                  const tagName = match[1];
                  const tagValue = match[2];
                  if (tagName !== 'Game' && tagName !== 'Format' && tagName !== 'FEN') {
                      pgnMetadata.push({ name: tagName, value: tagValue });
                  }
                  if (tagName === 'FEN') pgnFen = tagValue;
              }

              // Extract notation body
              let bodyStartIndex = text.lastIndexOf(']');
              let bodyText = text.substring(bodyStartIndex + 1).trim();
              bodyText = bodyText.replace(/(?:1-0|0-1|1\/2-1\/2|\*)$/, '').trim();

              // Tokenize body for moves, comments, and variation markers
              let tokens = [];
              let currentToken = '';
              let inComment = false;

              for (let i = 0; i < bodyText.length; i++) {
                  let char = bodyText[i];
                  if (inComment) {
                      if (char === '}') {
                          inComment = false;
                          tokens.push({ type: 'comment', value: currentToken.trim() });
                          currentToken = '';
                      } else {
                          currentToken += char;
                      }
                  } else {
                      if (char === '{') {
                          if (currentToken.trim() !== '') tokens.push({ type: 'text', value: currentToken.trim() });
                          currentToken = '';
                          inComment = true;
                      } else if (char === '(' || char === ')') {
                          if (currentToken.trim() !== '') tokens.push({ type: 'text', value: currentToken.trim() });
                          currentToken = '';
                          tokens.push({ type: char });
                      } else {
                          currentToken += char;
                      }
                  }
              }
              if (currentToken.trim() !== '') tokens.push({ type: 'text', value: currentToken.trim() });

              // Flatten into usable blocks
              let flatTokens = [];
              tokens.forEach(t => {
                  if (t.type === 'text') {
                      let parts = t.value.split(/\s+/);
                      parts.forEach(p => {
                          if (p !== '') flatTokens.push({ type: 'word', value: p });
                      });
                  } else {
                      flatTokens.push(t);
                  }
              });

              // Reconstruct Tree
              let rootNode = { fen: pgnFen, move: null, lastMove: null, c: '', v: [] };
              let currentNode = rootNode;
              let lastAddedNode = null;
              let isRedTurn = pgnFen.includes(' w');
              let varStack = [];

              let tIdx = 0;
              if (flatTokens.length > 0 && flatTokens[0].type === 'comment') {
                  rootNode.c = flatTokens[0].value;
                  tIdx++;
              }

              for (; tIdx < flatTokens.length; tIdx++) {
                  let token = flatTokens[tIdx];

                  if (token.type === 'word') {
                      let word = token.value;
                      if (/^\d+\.+$/.test(word) || word === '...' || word === '…') continue;

                      let dc = null;
                      let moveEn = '';
                      let isEnglish = /^[a-zA-Z]/.test(word) || /^[+\-=1-9][a-zA-Z0-9]/.test(word);

                      if (/^[a-i][0-9][a-i][0-9]$/i.test(word) || /^[A-I][0-9][A-I][0-9]$/i.test(word)) {
                          word = word.toLowerCase();
                          let startX = word.charCodeAt(0) - 97;
                          let startY = 9 - parseInt(word.charAt(1), 10);
                          let endX = word.charCodeAt(2) - 97;
                          let endY = 9 - parseInt(word.charAt(3), 10);
                          dc = { startX, startY, endX, endY };
                          let vMap = buildVirtualMap(currentNode.fen);
                          let pId = vMap.get(`${startX},${startY}`);
                          if (pId) {
                              moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                          } else {
                              moveEn = word;
                          }
                      } else {
                          moveEn = isEnglish ? word : NotationConverter.toEnglish(word, isRedTurn);
                          dc = deriveCoordsFromMove(currentNode.fen, moveEn, isRedTurn);
                      }

                      if (dc) {
                          let nextFen = simulateMove(currentNode.fen, dc);
                          let newNode = { fen: nextFen, move: moveEn, lastMove: dc, c: '', v: [], parent: currentNode };
                          currentNode.v.push(newNode);
                          lastAddedNode = newNode;
                          currentNode = newNode;
                          isRedTurn = !isRedTurn;
                      }
                  } else if (token.type === 'comment') {
                      if (lastAddedNode) {
                          if (lastAddedNode.c) lastAddedNode.c += '\n' + token.value;
                          else lastAddedNode.c = token.value;
                      } else {
                          if (rootNode.c) rootNode.c += '\n' + token.value;
                          else rootNode.c = token.value;
                      }
                  } else if (token.type === '(') {
                      varStack.push({ currentNode: currentNode, lastAddedNode: lastAddedNode, isRedTurn: isRedTurn });
                      if (lastAddedNode && lastAddedNode.parent) {
                          currentNode = lastAddedNode.parent;
                          isRedTurn = currentNode.fen.includes(' w');
                      }
                  } else if (token.type === ')') {
                      if (varStack.length > 0) {
                          let st = varStack.pop();
                          currentNode = st.currentNode;
                          lastAddedNode = st.lastAddedNode;
                          isRedTurn = st.isRedTurn;
                      }
                  }
              }

              // Strip temporary parent dependencies
              function cleanParents(node) {
                  delete node.parent;
                  if (node.v) node.v.forEach(cleanParents);
              }
              cleanParents(rootNode);

              historyFEN = rootNode;
              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;
          }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

發現如果我加上「所有的代碼和注釋都要使用英文」,然後gemini就會更強力超多。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

改進:
一,當每次打開pgn-meta-modal時,所有有值的name和value會放到最上面,下面將按順序顯示pgnMetadata的name(排除上面已經有的)以及空value。

二,當按下pgn-meta-modal中的default鍵時,改為前面全部按pgnMetadata的name進行顯示和排序。如果之前有值的,則會填入相應的value中。如果有name跟pgnMetadata不一樣的,則加到最下面。

三,PGN格式中的Result,如果值是「*」,則等同於空,在導出文本時不會把[Result "*"]標籤顯示出來,在打開pgn-meta-modal時當作空值處理。

四,在保存為JSON時,pgnMetadata要直接把name當作key,如"meta":[{"Event":"something"},{"Date":"2026.04.26"}],只保存有值的,值為空的(包括Result為*的)將不保存。當在pgn-meta-modal中增加或修改name時,要保證其能用於JSON的object的key。

只需給出所要修改的地方,所有代碼和注釋都要使用英文。
附件
ejcees20260425pgn5.7z
(51.03 KiB) 已下载 19 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro 啃個硬的:

代码: 全选

Here are the exact modifications needed to implement the requested features. All code and comments are provided in English.

### 1. Define Default Keys (Global Scope)
Find the `pgnMetadata` initialization near line `780` and add the `defaultPgnKeys` array right below it for sorting reference.

```javascript
      let pgnMetadata = [
        { name: 'Event', value: '' },
        { name: 'Site', value: '' },
        { name: 'Date', value: '' },
        { name: 'Round', value: '' },
        { name: 'RedTeam', value: '' },
        { name: 'Red', value: '' },
        { name: 'BlackTeam', value: '' },
        { name: 'Black', value: '' },
        { name: 'Result', value: '' },
        { name: 'Time', value: '' },
        { name: 'TimeControl', value: '' },
        { name: 'Opening', value: '' },
        { name: 'Variation', value: '' },
        { name: 'ECCO', value: '' },
        { name: 'Annotator', value: '' },
        { name: 'Termination', value: '' }
      ];

      // Added: Default PGN keys array for sorting and resetting (Improvement 1 & 2)
      const defaultPgnKeys = [
        'Event', 'Site', 'Date', 'Round', 'RedTeam', 'Red', 'BlackTeam', 'Black',
        'Result', 'Time', 'TimeControl', 'Opening', 'Variation', 'ECCO', 'Annotator', 'Termination'
      ];
      let pgnSelectedRowIndex = -1;
```

---

### 2. Update the `showPGNMetaModal` Function
Replace the entire `showPGNMetaModal()` function (starting around line `1610`) with the following code to implement the sorting (Improvement 1), the Default button behavior (Improvement 2), and the input restriction (Improvement 4).

```javascript
        function showPGNMetaModal() {
          const modal = document.createElement('div');
          modal.className = 'pgn-meta-modal';

          // Improvement 1: Sort items (non-empty at top, empty defaults below, preserving order)
          let withValue = [];
          let emptyDefaults = [];
          let addedKeys = new Set();

          pgnMetadata.forEach(item => {
            let val = item.value ? item.value.trim() : '';
            // Improvement 3: Treat '*' for Result as an empty value
            let isResultEmpty = (item.name === 'Result' && val === '*');
            if (val !== '' && !isResultEmpty) {
              withValue.push({ name: item.name, value: item.value });
              addedKeys.add(item.name);
            }
          });

          // Add defaults in original order if not already added
          defaultPgnKeys.forEach(key => {
            if (!addedKeys.has(key)) {
              emptyDefaults.push({ name: key, value: (key === 'Result' ? '*' : '') });
              addedKeys.add(key);
            }
          });

          // Add any remaining custom fields that were empty
          pgnMetadata.forEach(item => {
            if (!addedKeys.has(item.name)) {
              emptyDefaults.push({ name: item.name, value: item.value });
              addedKeys.add(item.name);
            }
          });

          let localMetadata = withValue.concat(emptyDefaults);

          const box = document.createElement('div');
          box.className = 'pgn-meta-box';

          const headerDiv = document.createElement('div');
          headerDiv.style.display = 'flex';
          headerDiv.style.justifyContent = 'space-between';
          headerDiv.style.alignItems = 'center';

          const title = document.createElement('h3');
          title.textContent = 'PGN Metadata';

          // Improvement 2: Default button reorders elements according to the default list
          const defaultBtn = document.createElement('button');
          defaultBtn.textContent = 'Default';
          defaultBtn.style.padding = '4px 8px';
          defaultBtn.style.cursor = 'pointer';
          defaultBtn.style.marginRight = '30px'; 
          defaultBtn.onclick = () => {
            syncDOM(); 
            let newLocal = [];
            let currentMap = new Map();
            localMetadata.forEach(item => currentMap.set(item.name, item.value));

            // Add all default keys in their proper sequence
            defaultPgnKeys.forEach(key => {
              let val = currentMap.has(key) ? currentMap.get(key) : (key === 'Result' ? '*' : '');
              newLocal.push({ name: key, value: val });
              currentMap.delete(key);
            });

            // Append any remaining custom keys at the bottom
            currentMap.forEach((val, key) => {
              newLocal.push({ name: key, value: val });
            });

            localMetadata = newLocal;
            renderTable();
          };

          headerDiv.appendChild(title);
          headerDiv.appendChild(defaultBtn);

          function syncDOM() {
            const rows = tbody.querySelectorAll('.pgn-meta-row');
            localMetadata = [];
            rows.forEach((tr) => {
              const name = tr.querySelector('td:nth-child(2) input').value;
              const valEl = tr.querySelector('td:nth-child(3) input, td:nth-child(3) select');
              const value = valEl ? valEl.value : '';
              localMetadata.push({ name, value });
            });
          }

          const closeBtn = document.createElement('button');
          closeBtn.className = 'close-btn';
          closeBtn.style.top = '10px';
          closeBtn.style.right = '10px';
          closeBtn.onclick = () => {
            syncDOM(); 
            const isChanged = JSON.stringify(localMetadata) !== JSON.stringify(pgnMetadata);
            if (isChanged) {
              pgnMetadata = JSON.parse(JSON.stringify(localMetadata));
            }

            document.body.removeChild(modal);

            if (isExportTextMode) {
              const activeBtn = document.querySelector('.exp-fmt-btn.active');
              const oddState = activeBtn ? parseInt(activeBtn.getAttribute('data-odd') || '0') : 0;
              const exportArea = document.querySelector('.ejceescomment-edit');
              if (exportArea) {
                exportArea.value = generatePGNFormat(oddState);
              }
            }
            if (isChanged) saveStateToUndo();
          };

          const table = document.createElement('table');
          table.className = 'pgn-meta-table';
          const thead = document.createElement('thead');
          thead.innerHTML = `<tr><th>No.</th><th>Name</th><th>Value</th><th>Remove</th></tr>`;
          const tbody = document.createElement('tbody');
          table.appendChild(thead);
          table.appendChild(tbody);

          function renderTable() {
            tbody.innerHTML = '';
            const highlights = ['Event', 'Site', 'Date', 'Round', 'Red', 'Black', 'Result'];
            localMetadata.forEach((item, index) => {
              const tr = document.createElement('tr');
              tr.className = 'pgn-meta-row';
              if (pgnSelectedRowIndex === index) tr.classList.add('selected');

              const tdNum = document.createElement('td');
              tdNum.textContent = index + 1;
              tdNum.style.cursor = 'pointer';
              tdNum.onclick = () => {
                syncDOM(); 
                if (pgnSelectedRowIndex === -1) {
                  pgnSelectedRowIndex = index;
                } else if (pgnSelectedRowIndex === index) {
                  pgnSelectedRowIndex = -1;
                } else {
                  const movedItem = localMetadata.splice(pgnSelectedRowIndex, 1)[0];
                  localMetadata.splice(index, 0, movedItem);
                  pgnSelectedRowIndex = -1;
                }
                renderTable();
              };
              tr.appendChild(tdNum);

              const tdName = document.createElement('td');
              const nameInput = document.createElement('input');
              nameInput.className = 'pgn-meta-input';
              if (highlights.includes(item.name)) nameInput.classList.add('pgn-meta-highlight');
              nameInput.value = item.name;
              
              // Improvement 4: Ensure name can be used as a valid JSON key
              nameInput.oninput = (e) => {
                e.target.value = e.target.value.replace(/[^a-zA-Z0-9_]/g, '');
              };

              tdName.appendChild(nameInput);
              tr.appendChild(tdName);

              const tdValue = document.createElement('td');
              if (item.name === 'Result') {
                const sel = document.createElement('select');
                sel.className = 'pgn-meta-input';
                ['*', '1-0', '0-1', '1/2-1/2'].forEach((opt) => {
                  const option = document.createElement('option');
                  option.value = opt;
                  option.textContent = opt;
                  if (opt === item.value) option.selected = true;
                  sel.appendChild(option);
                });
                tdValue.appendChild(sel);
              } else {
                const valInput = document.createElement('input');
                valInput.className = 'pgn-meta-input';
                valInput.value = item.value;
                tdValue.appendChild(valInput);
              }
              tr.appendChild(tdValue);

              const tdDel = document.createElement('td');
              tdDel.className = 'pgn-meta-trash';
              tdDel.textContent = '🗑️';
              tdDel.onclick = () => {
                syncDOM();
                localMetadata.splice(index, 1);
                if (pgnSelectedRowIndex === index) pgnSelectedRowIndex = -1;
                else if (pgnSelectedRowIndex > index) pgnSelectedRowIndex--;
                renderTable();
              };
              tr.appendChild(tdDel);
              tbody.appendChild(tr);
            });
          }

          renderTable();

          const addBtn = document.createElement('div');
          addBtn.className = 'pgn-add-btn';
          addBtn.textContent = '+';
          addBtn.onclick = () => {
            syncDOM();
            localMetadata.push({ name: '', value: '' });
            renderTable();
          };

          box.appendChild(closeBtn);
          box.appendChild(headerDiv);
          box.appendChild(table);
          box.appendChild(addBtn);
          modal.appendChild(box);
          document.body.appendChild(modal);
        }
```

---

### 3. Update PGN Text Export (`generatePGNFormat`)
Modify the `generatePGNFormat` function (around line `1738`) to exclude the `[Result "*"]` tag from text export (Improvement 3).

```javascript
        function generatePGNFormat(oddState) {
          let text = '[Game "Chinese Chess"]\n';
          let userResult = '';

          pgnMetadata.forEach((meta) => {
            let val = meta.value ? meta.value.trim() : '';
            // Improvement 3: Treat '*' for Result as empty, skip output
            let isResultEmpty = (meta.name === 'Result' && val === '*');
            
            if (val !== '' && !isResultEmpty) {
              text += `[${meta.name} "${val}"]\n`;
              if (meta.name === 'Result') {
                userResult = val;
              }
            }
          });
          
          // ... (rest of the generatePGNFormat function remains unchanged)
```

---

### 4. Create Helper for JSON Import
To support the new format where JSON provides an array of `{ "Key": "Value" }` objects, add this helper function directly above `document.getElementById('tool-save').addEventListener` (around line `905`).

```javascript
        // Helper to load JSON array of key-value objects back into internal pgnMetadata structure
        function loadPGNMetadataFromJSON(metaArray) {
          let newMeta = defaultPgnKeys.map(k => ({ name: k, value: (k === 'Result' ? '*' : '') }));
          let loadedKeys = new Set();

          metaArray.forEach(obj => {
            for (let key in obj) {
              if (Object.prototype.hasOwnProperty.call(obj, key)) {
                let existing = newMeta.find(m => m.name === key);
                if (existing) {
                  existing.value = obj[key];
                } else {
                  newMeta.push({ name: key, value: obj[key] });
                }
                loadedKeys.add(key);
              }
            }
          });
          pgnMetadata = newMeta;
        }
```

---

### 5. Update JSON Export & Import (Improvement 4)
First, modify the save logic inside `document.getElementById('tool-save').addEventListener` (around line `930`):

```javascript
          // Improvement 4: Append PGN metadata to JSON payload using the name as the key.
          // Only save items with values (Result '*' is excluded).
          const validMeta = pgnMetadata.filter(m => {
            let val = m.value ? m.value.trim() : '';
            return m.name && val !== '' && !(m.name === 'Result' && val === '*');
          });

          if (validMeta.length > 0) {
            copy.meta = validMeta.map(m => {
              let obj = {};
              let safeKey = m.name.replace(/[^a-zA-Z0-9_]/g, '');
              if (!safeKey) safeKey = "Unknown";
              obj[safeKey] = m.value;
              return obj;
            });
          }

          const blob = new Blob([JSON.stringify(copy)], {
            type: 'text/plain'
          });
```

Second, find the file load parser inside `document.getElementById('file-input').addEventListener` (around line `960`) and update it to use the new helper:

```javascript
            // Read PGN metadata from the parsed JSON payload
            if (data.meta && Array.isArray(data.meta)) {
              loadPGNMetadataFromJSON(data.meta);
            }
```

Finally, find the paste logic inside `importExportedText()` (around line `2110`) when processing `if (text.startsWith('{'))` and update it as well:

```javascript
              // Load PGN metadata on JSON paste
              if (data.meta && Array.isArray(data.meta)) {
                loadPGNMetadataFromJSON(data.meta);
              }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

改進:
一,pgnMetadata的所有有值的項,都應該在點擊pgn-meta-modal的close-btn時保存到localstorage和undo-stack中。

二,當點擊tool-exp-txt打開導出窗口時,按ctrl+1、ctrl+2、ctrl+3、ctrl+4,分別等同於按「DP Format、PGN Format、Engine Format、URL Parameter」。

三,當點擊tool-exp-txt打開導出窗口,並處於PGN Format時,按ctrl+0,則會彈出pgn-meta-modal。

只需給出所要修改的地方,所有代碼和注釋都要使用英文。
附件
ejcees20260426pgn6.7z
(51.65 KiB) 已下载 17 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro還能用:

代码: 全选

Here are the modifications to implement the requested improvements. 

### 1. Update `showPGNMetaModal` Close Button Logic (Improvement 1)
Find the `closeBtn.onclick` handler inside the `showPGNMetaModal()` function. Replace it with this code to ensure only items with actual values are kept in `pgnMetadata` before saving to the undo stack and `localStorage`.

```javascript
          closeBtn.onclick = () => {
            syncDOM(); 
            
            // Improvement 1: Only keep items with a value (Result '*' is treated as empty)
            const newPgnMetadata = localMetadata.filter(item => {
              const val = item.value ? item.value.trim() : '';
              return item.name && val !== '' && !(item.name === 'Result' && val === '*');
            });

            const isChanged = JSON.stringify(newPgnMetadata) !== JSON.stringify(pgnMetadata);
            if (isChanged) {
              pgnMetadata = JSON.parse(JSON.stringify(newPgnMetadata));
            }

            document.body.removeChild(modal);

            if (isExportTextMode) {
              const activeBtn = document.querySelector('.exp-fmt-btn.active');
              const oddState = activeBtn ? parseInt(activeBtn.getAttribute('data-odd') || '0') : 0;
              const exportArea = document.querySelector('.ejceescomment-edit');
              if (exportArea) {
                exportArea.value = generatePGNFormat(oddState);
              }
            }
            
            // This triggers the save to undo-stack and localStorage
            if (isChanged) saveStateToUndo();
          };
```

### 2. Include `pgnMetadata` in the Undo-Stack and Initialization (Improvement 1)
To ensure `saveStateToUndo()` actually saves `pgnMetadata` into the stack and `localStorage`, you need to add it to the state object.

**A. In your `saveStateToUndo()` function:**
Add `pgnMetadata` to the state object that gets pushed to the `undoStack`.
```javascript
        function saveStateToUndo() {
          // ... existing code ...
          const state = {
            historyFEN: JSON.parse(JSON.stringify(historyFEN)),
            currentBranch: [...currentBranch],
            currentStepIndex: currentStepIndex,
            moveInterval: moveInterval,
            moveSpeed: moveSpeed,
            isRotateEnabled: isRotateEnabled,
            isFlipEnabled: isFlipEnabled,
            // Add this line below to save metadata:
            pgnMetadata: JSON.parse(JSON.stringify(pgnMetadata))
          };
          // ... existing code ...
```

**B. In your `undo()` and `redo()` functions:**
Where you retrieve the `state` object, add this line to restore `pgnMetadata`.
```javascript
          // Add this line inside both undo() and redo() when state is restored:
          pgnMetadata = state.pgnMetadata ? JSON.parse(JSON.stringify(state.pgnMetadata)) : [];
```

**C. In your `DOMContentLoaded` initialization block:**
Find the `// 2. Initialize with resolved state or default FEN` section near the end of your script, and add `pgnMetadata`.
```javascript
        // 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;
          // Add this line to load saved metadata on startup:
          pgnMetadata = initialHistory.pgnMetadata || [];
          
          loadFEN(historyFEN.fen);
```

### 3. Add Keyboard Shortcuts for Export Text Modal (Improvements 2 & 3)
Add this global `keydown` event listener to handle the `Ctrl + Number` shortcuts. You can place this block near the bottom of your `<script>`, just before `window.addEventListener('DOMContentLoaded', ...);`.

```javascript
      // Improvements 2 & 3: Keyboard shortcuts for Export Text Modal
      document.addEventListener('keydown', (e) => {
        // Check if the export modal is active and Ctrl is pressed
        if (typeof isExportTextMode !== 'undefined' && isExportTextMode && e.ctrlKey) {
          const formatMap = {
            '1': 'DP Format',
            '2': 'PGN Format',
            '3': 'Engine Format',
            '4': 'URL Parameter'
          };

          if (formatMap[e.key]) {
            // Improvement 2: Ctrl + 1/2/3/4 switches formats
            e.preventDefault();
            const btns = Array.from(document.querySelectorAll('.exp-fmt-btn'));
            const targetBtn = btns.find(b => b.textContent.includes(formatMap[e.key]));
            if (targetBtn) targetBtn.click();
          } else if (e.key === '0') {
            // Improvement 3: Ctrl + 0 opens pgn-meta-modal if PGN Format is active
            e.preventDefault();
            const pgnBtn = Array.from(document.querySelectorAll('.exp-fmt-btn')).find(b => b.textContent.includes('PGN Format'));
            if (pgnBtn && pgnBtn.classList.contains('active')) {
              if (typeof showPGNMetaModal === 'function') {
                showPGNMetaModal();
              }
            }
          }
        }
      });
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

ejcees20260426pgn7

新增pgn格式(包括導入和導出),將pgn的信息保存至json以及localstorage中,增加快捷鍵打開exp-txt中的每一項以及pgn的設置窗口。

https://ejsoon.vip/wp-content/uploads/2 ... 6pgn7.html
附件
ejcees20260426pgn7.7z
(51.92 KiB) 已下载 20 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

正浏览此版面之用户: Bing [Bot] 和 0 访客