工欲善其事,必先利其器:準備更新哈哈倉頡

分享和討論倉頡輸入法相關的資訊和内容
版面规则
公正客觀講理,杜絶廢話連篇
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5701
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

帖子 ejsoon »

為什麼在單字模式中,點擊一個edit-item,再點擊另一個,這個字沒有移到另一個字的前面或後面呢?


gemini pro:

代码: 全选

這是一個非常敏銳的觀察!這確實是程式碼邏輯中的一個漏洞。

這個字沒有移動的原因在於**檢測條件放錯了位置**。在原先的程式碼中,判斷兩個字能否移動的邏輯被包在一個大括號裡:它去搜尋 `mcc0string`(這是一個專門存放**詞組**的陣列)來找你點擊的字。

但在**單字模式**中,你點擊的是**單一漢字**,這些字存在於 `charDefCodeMap` 而不是 `mcc0string`。因此,程式在詞組名單裡找不到這兩個漢字,返回了 `-1`,導致整個移動邏輯直接被跳過(Bypassed),什麼事都沒發生。

為了解決這個問題,我們需要把「單字模式」與「詞組模式」的搜尋邏輯完全分開。

以下是完整的修正代碼。請替換掉你原來的 `handleItemClick` 函數:

### Update `handleItemClick`

```javascript
function handleItemClick(word) {
  if (pendingAction) {
    applyActionToWord(pendingAction, word);
    pendingAction = null;
    selectedEditItem = null;
    updateActionButtonsUI();
    return;
  }

  if (!selectedEditItem) {
    selectedEditItem = word;
    justMovedItem = null; // Clear previous just-moved styling when making a new selection
    renderEditArea();
  } else {
    if (selectedEditItem === word) {
      selectedEditItem = null;
      renderEditArea();
      return;
    }

    let oldState = getSystemState();

    if (isSingleWordMode) {
      // --- SINGLE WORD MODE LOGIC ---
      // Find which code row this word belongs to within active keys
      let targetCode = Array.from(activeEditKeys).find(k => (charDefCodeMap.get(k) || []).includes(word));
      let sourceCode = Array.from(activeEditKeys).find(k => (charDefCodeMap.get(k) || []).includes(selectedEditItem));

      if (targetCode && targetCode === sourceCode) {
        let arr = charDefCodeMap.get(targetCode);
        let charIdxA = arr.indexOf(selectedEditItem);
        let charIdxB = arr.indexOf(word);

        // Check array bounds specific to single characters
        if (charIdxA > -1 && charIdxB > -1 && charIdxA !== charIdxB) {
          // Format descriptive log: "Word A -> before/after Word B"
          let placement = charIdxA > charIdxB ? 'before' : 'after';
          saveStateForUndo(`${selectedEditItem} -> ${placement} ${word}`);

          arr.splice(charIdxA, 1);
          let newIdxB = arr.indexOf(word);
          arr.splice(charIdxA > charIdxB ? newIdxB : newIdxB + 1, 0, selectedEditItem);
          justMovedItem = selectedEditItem;

          // Rebuild %chardef block in filecontent directly to persist single word moves
          let linesFile = filecontent.split('\n');
          let chardefBeginIdx = linesFile.indexOf('%chardef begin');
          let chardefEndIdx = -1;
          let inDef = false;

          for (let i = 0; i < linesFile.length; i++) {
            if (linesFile[i] === '%chardef begin') {
              inDef = true;
              continue;
            }
            if (!inDef) continue;
            if (
              linesFile[i] === '%chardef end' ||
              linesFile[i].startsWith('x') ||
              /[^z]z/.test(linesFile[i].split('\t')[0])
            ) {
              chardefEndIdx = i;
              break;
            }
          }

          if (chardefBeginIdx > -1 && chardefEndIdx > -1) {
            let newBlock = [];
            for (let [c, chars] of charDefCodeMap) {
              chars.forEach(ch => newBlock.push(`${c}\t${ch}`));
            }
            linesFile.splice(chardefBeginIdx + 1, chardefEndIdx - chardefBeginIdx - 1, ...newBlock);
            filecontent = linesFile.join('\n');

            // Execute global processing phase to synchronize state and refresh UI structures
            processContent();
            let newState = getSystemState();
            detectAndRenderChanges(oldState, newState);
          }
          renderEditArea();
        }
      }
    } else {
      // --- PHRASE MODE LOGIC ---
      let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
      let originalIdxA = lines.indexOf(selectedEditItem);
      let originalIdxB = lines.indexOf(word);

      // Check array bounds specific to phrases
      if (originalIdxA > -1 && originalIdxB > -1 && originalIdxA !== originalIdxB) {
        let placement = originalIdxA > originalIdxB ? 'before' : 'after';
        saveStateForUndo(`${selectedEditItem} -> ${placement} ${word}`);

        // Extract A from lines
        lines.splice(originalIdxA, 1);

        // Recalculate target position index for element B after A is removed
        let newIdxB = lines.indexOf(word);

        // If A was originally after B, move A to be before B
        if (originalIdxA > originalIdxB) {
          lines.splice(newIdxB, 0, selectedEditItem);
        }
        // If A was originally before B, move A to be after B
        else {
          lines.splice(newIdxB + 1, 0, selectedEditItem);
        }

        justMovedItem = selectedEditItem; // Target item now registers as just-moved
        processContent(JSON.stringify(lines));
        let newState = getSystemState();
        detectAndRenderChanges(oldState, newState);
      }
    }
    
    selectedEditItem = null; // Deselect element once movement completes
  }
}

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5701
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

帖子 ejsoon »

簡而言之,兩個程式在載入文本文件之後,都執行了哪些函式?

代码: 全选

簡而言之,這兩個程式在載入文本文件(透過檔案上傳欄位的 `change` 事件)後,核心執行的 JavaScript 函式流程如下:

### 1. 第一個程式 (`domchar5741.html`:碼表詞組處理)

當使用者選取檔案後,主要依序執行了以下函式:

* **`handleFile(event)`**:
* 負責接收檔案物件,隱藏上傳標籤。
* 建立 `FileReader`,並在讀取完成後觸發呼叫 `processContent()`。


* **`processContent()`**(核心業務函式):
* 內部分別清空所有 Map 容器。
* 將文字切行,解析 `%chardef begin` 到碼表結束之間的字位定義,並將首碼、次碼存入 `ccharfirst` 與 `ccharsecond`。
* 篩選、去重並擷取詞組總表(`mcc0string`),再依據編碼與長度規則將詞組分類至 `mcc0` ~ `mcc4` 的各個 Map 中。
* 自動計算當前時間戳記,組合出新文字,轉換成 Blob 並更新下載連結。
* 最後將主界面分頁系統顯示出來。


* *備註:網頁中雖有名為 `resetPage()` 的函式,但它是用於清空與還原初始狀態,在初次載入檔案時不會主動執行。*

---

### 2. 第二個程式 (`domu1.html`:重碼分析工具)

當使用者選取檔案後,主要依序執行了以下函式:

* **(匿名事件監聽函式)**:
* 在 `fileInput` 的 `change` 事件監聽器中,負責更新畫面的選取狀態提示,並直接呼叫 `analyzeFile()`。


* **`analyzeFile()`**(主體異步函式):
* 使用 `file.text()` 異步讀取純文字。
* 利用正則表達式清洗行尾,並尋找 `z` 或 `x` 開頭的臨界點以確定有效範圍。
* 解析編碼並將漢字存入 `codeMap`,並完成重碼與碼長的分類統計。
* 計算魔改後綴(消重碼),將結果分別輸出到網頁上的多個 `textarea` 文本框中。
* 最後呼叫 `integrateDownload()`。


* **`integrateDownload()`**(文件整合與導出函式):
* 負責獲取當前系統時間戳記、更新 `%version`。
* 將原本碼表的漢字定義區塊替換為排序後的規格,並在 `x` 開頭行前插入魔改去重數據(`gzhaha`)。
* 轉換為 Blob 並動態生成最終的整合下載連結。



### 總結

* **第一個程式**:觸發 `handleFile` $\rightarrow$ 呼叫 `processContent`(一擊完成所有解析與下載準備)。
* **第二個程式**:觸發監聽器 $\rightarrow$ 呼叫 `analyzeFile` $\rightarrow$ 內部再呼叫 `integrateDownload`(分工完成分析與文件重組)。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5701
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

帖子 ejsoon »

把domu1.html的功能合並至domchar5741.html中,按以下要求進行:
一,將domu1.html中的analyzeFile複製過來。當文本載入時,先執行analyzeFile函式(不用生成download),之後再把結果交給processContent函式。

二,把#output和#download,擴展為一個美觀的概覽,它的項目為:單字數量(cchardefine 行數),尹末去重(開頭不是z或x,後面含有z的行數),中文標點(開頭為x但第二個字母不為z的行數),英文符號(開頭為xz的行數),詞組數量(mcc0string 行數),詞組去重(所有以z開頭的行數減去mcc0string 行數)。下載按鈕添加美觀的css。

三,在Submit右邊加上「自動、單字、詞語」的切換按鈕,默認為自動。

當處於自動模式時,如果在codeInput所輸入的是英文字母且開頭不是z,或者輸入的是一個漢字,則自動判斷是單字模式(不再在英文字母前自動加z。);如果輸入的是英文字母且開頭是z,或者輸入的是兩個以上的漢字,則自動判斷是詞語模式。

當按下enter或點擊Submit後,模式切換按鈕切到「單字」或「詞語」,這時模式將不能再改變,除非按下btnCloseEdit。

四,當處於單字模式時,如果codeInput輸入的是一個漢字,則展示這個字所在的序列的重碼情況。

例如輸入「轟」(或者jjjj),則下方將顯示:
j 都十
jj 幹艹卄廾
jjj 轉
jjjj 轟

當點擊其中一個字A,它將會高亮,點擊另一個字B,如果A在B的前面,則A將插入至B的後面;如果A在B的後面,則A將插入至B的前面。之後執行analyzeFile函式,統計數據更新,並生成download。

這些操作都將計入undo-redo,reset,操作log中。

如果codeInput輸入的是一個編碼,則這個編碼作為標題將高亮。

當一個字從一個欄目移到另一個欄目,則它的編碼將變為所在欄的標題的編碼。

當一個編碼本身沒有字(空碼),或者一開始有字,但是後來被移走了,則它下方(原本有字的地方)將出現一個用方框圍住的加號,用以其它單字再移過去。

單字模式沒有「put to the end」和「delete」。其餘跟詞語模式類似。

五,把analyzeFile中的統計功能,加至Statistics欄,其最左邊增加一個切換按鈕「重碼統計」,下方將是analyzeFile中的統計結果。「重碼統計」將是Statistics欄默認顯示的項目。

「總重碼統計」將改為:0重、1重、2重……。

「分類重碼統計」將改為:碼長1:0重、1重、2重……;碼長2:0重、1重、2重……;碼長3:0重、1重、2重……;碼長4:0重、1重、2重……。

將「重碼結果」和「去重結果」標題後面加上數量,去掉「下載」。把四個「超出字」按之前的順序合並成一個,數量也合並。

美化以上元素的css,不要用原生的html樣式。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

代码: 全选

把domu1.html的功能合並至domchar5741.html中,按以下要求進行:
一,將domu1.html中的analyzeFile複製過來。當文本載入時,先執行analyzeFile函式(不用生成download),之後再把結果交給processContent函式。

二,把#output和#download,擴展為一個美觀的概覽,它的項目為:單字數量(cchardefine 行數),尹末去重(開頭不是z或x,後面含有z的行數),中文標點(開頭為x但第二個字母不為z的行數),英文符號(開頭為xz的行數),詞組數量(mcc0string 行數),詞組去重(所有以z開頭的行數減去mcc0string 行數)。下載按鈕添加美觀的css。

三,在Submit右邊加上「自動、單字、詞語」的切換按鈕,默認為自動。

當處於自動模式時,如果在codeInput所輸入的是英文字母且開頭不是z,或者輸入的是一個漢字,則自動判斷是單字模式(不再在英文字母前自動加z。);如果輸入的是英文字母且開頭是z,或者輸入的是兩個以上的漢字,則自動判斷是詞語模式。

當按下enter或點擊Submit後,模式切換按鈕切到「單字」或「詞語」,這時模式將不能再改變,除非按下btnCloseEdit。

四,當處於單字模式時,如果codeInput輸入的是一個漢字,則展示這個字所在的序列的重碼情況。

例如輸入「轟」(或者jjjj),則下方將顯示:
j	都十
jj	幹艹卄廾
jjj	轉
jjjj	轟

當點擊其中一個字A,它將會高亮,點擊另一個字B,如果A在B的前面,則A將插入至B的後面;如果A在B的後面,則A將插入至B的前面。之後執行analyzeFile函式,統計數據更新,並生成download。

這些操作都將計入undo-redo,reset,操作log中。

如果codeInput輸入的是一個編碼,則這個編碼作為標題將高亮。

當一個字從一個欄目移到另一個欄目,則它的編碼將變為所在欄的標題的編碼。

當一個編碼本身沒有字(空碼),或者一開始有字,但是後來被移走了,則它下方(原本有字的地方)將出現一個用方框圍住的加號,用以其它單字再移過去。

單字模式沒有「put to the end」和「delete」。其餘跟詞語模式類似。

五,把analyzeFile中的統計功能,加至Statistics欄,其最左邊增加一個切換按鈕「重碼統計」,下方將是analyzeFile中的統計結果。「重碼統計」將是Statistics欄默認顯示的項目。

「總重碼統計」將改為:0重、1重、2重……。

「分類重碼統計」將改為:碼長1:0重、1重、2重……;碼長2:0重、1重、2重……;碼長3:0重、1重、2重……;碼長4:0重、1重、2重……。

將「重碼結果」和「去重結果」標題後面加上數量,去掉「下載」。把四個「超出字」按之前的順序合並成一個,數量也合並。

美化以上元素的css,不要用原生的html樣式。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5701
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

帖子 ejsoon »

gemini pro:

代码: 全选

Here are the specific modifications required to merge the features from `domu1.html` into `domchar5741.html` according to your requirements. All code and comments are provided in English.

### 1. HTML Modifications

**Find this block:**

```html
    <div id="output"></div>
    <div id="download"></div>
    <br />

```

**Replace with:**

```html
    <div id="dashboardOverview" class="dashboard-grid" style="display: none;">
      <div class="stat-card">
        <div class="stat-title">Single Char Count</div>
        <div class="stat-value" id="dashCharDef">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">Yinmo Dedup</div>
        <div class="stat-value" id="dashYinmo">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">Chinese Punc</div>
        <div class="stat-value" id="dashChiPunc">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">English Sym</div>
        <div class="stat-value" id="dashEngSym">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">Phrase Count</div>
        <div class="stat-value" id="dashPhraseCount">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">Phrase Dedup</div>
        <div class="stat-value" id="dashPhraseDedup">0</div>
      </div>
    </div>
    <div id="downloadContainer" style="display: none; margin-top: 15px;"></div>

```

**Find this block:**

```html
        <div id="inputArea">
          <label for="codeInput" style="font-weight: bold">Encode Input: </label>
          <input type="text" id="codeInput" placeholder="Enter code (e.g. zhy) or word" />
          <button id="submitCode" onclick="handleEditSubmit()">Submit</button>
        </div>

```

**Replace with:**

```html
        <div id="inputArea">
          <label for="codeInput" style="font-weight: bold">Input: </label>
          <select id="modeSelect" class="mode-dropdown">
            <option value="auto">Auto</option>
            <option value="char">Char</option>
            <option value="phrase">Phrase</option>
          </select>
          <input type="text" id="codeInput" placeholder="Enter code or word" />
          <button id="submitCode" onclick="handleEditSubmit()">Submit</button>
        </div>

```

**Find this block:**

```html
        <div class="stats-buttons" id="statsBtnContainer">
          <button class="stats-btn" id="btnMcc1All" onclick="renderStatsSection('mcc1all', '二碼高頻', this)">

```

**Replace with:**

```html
        <div class="stats-buttons" id="statsBtnContainer">
          <button class="stats-btn active" id="btnDupStats" onclick="showDupStatsSection(this)">
            重碼統計
          </button>
          <button class="stats-btn" id="btnMcc1All" onclick="renderStatsSection('mcc1all', '二碼高頻', this)">

```

### 2. CSS Modifications

**Add the following CSS rules inside your `<style>` block:**

```css
      /* Dashboard Overview Styling */
      .dashboard-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
        gap: 15px;
        margin: 20px 0;
      }
      .stat-card {
        background: linear-gradient(135deg, #ffffff 0%, #f5f7fa 100%);
        border: 1px solid #e4e7eb;
        border-radius: 10px;
        padding: 15px;
        text-align: center;
        box-shadow: 0 4px 6px rgba(0,0,0,0.05);
        transition: transform 0.2s;
      }
      .stat-card:hover {
        transform: translateY(-2px);
        box-shadow: 0 6px 12px rgba(0,0,0,0.1);
      }
      .stat-title {
        font-size: 13px;
        color: #6b7280;
        margin-bottom: 8px;
        font-weight: bold;
      }
      .stat-value {
        font-size: 24px;
        color: #1f2937;
        font-weight: 900;
      }
      /* Beautiful Download Button */
      .modern-download-btn {
        display: inline-block;
        width: 100%;
        padding: 12px 24px;
        background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
        color: white !important;
        text-decoration: none;
        text-align: center;
        font-weight: bold;
        border-radius: 8px;
        box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
        transition: all 0.3s ease;
      }
      .modern-download-btn:hover {
        background: linear-gradient(135deg, #4338ca 0%, #6d28d9 100%);
        box-shadow: 0 6px 20px rgba(124, 58, 237, 0.4);
        transform: translateY(-1px);
      }
      /* Dropdown styling */
      .mode-dropdown {
        padding: 8px;
        border-radius: 4px;
        border: 1px solid #ccc;
        font-size: 14px;
        margin-right: 6px;
        outline: none;
      }
      .mode-dropdown:disabled {
        background-color: #f3f4f6;
        cursor: not-allowed;
      }
      /* Char mode specific elements */
      .empty-slot {
        display: inline-flex;
        justify-content: center;
        align-items: center;
        width: 30px;
        height: 30px;
        margin: 4px;
        border: 2px dashed #9ca3af;
        border-radius: 4px;
        color: #9ca3af;
        cursor: pointer;
        font-weight: bold;
        background: #f9fafb;
        transition: all 0.2s;
        vertical-align: middle;
      }
      .empty-slot:hover {
        background: #e5e7eb;
        border-color: #6b7280;
        color: #4b5563;
      }
      /* Beautified Dup Stats Area */
      .dup-stats-container {
        display: flex;
        flex-direction: column;
        gap: 20px;
      }
      .dup-box {
        background: #fff;
        border: 1px solid #e5e7eb;
        border-radius: 8px;
        padding: 15px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.02);
      }
      .dup-box h3 {
        margin-top: 0;
        margin-bottom: 10px;
        font-size: 16px;
        color: #374151;
        border-bottom: 2px solid #f3f4f6;
        padding-bottom: 8px;
      }
      .dup-row {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
      }
      .dup-tag {
        background: #f3f4f6;
        padding: 4px 10px;
        border-radius: 12px;
        font-size: 13px;
        color: #4b5563;
      }
      .dup-tag strong {
        color: #1f2937;
      }
      .modern-textarea {
        width: 100%;
        height: 120px;
        border: 1px solid #d1d5db;
        border-radius: 6px;
        padding: 10px;
        font-family: monospace;
        resize: vertical;
        box-sizing: border-box;
      }
      .highlighted-title {
        color: #d32f2f !important;
        background: #ffebee;
        padding: 2px 6px;
        border-radius: 4px;
      }

```

### 3. JavaScript Modifications

**Inject these Global Variables near the top of the `<script>` tag:**

```javascript
      // --- New Globals for Char Mode & Dup Stats ---
      let globalCodeMap = new Map();
      let rawCharDefLines = [];
      let activeMode = 'phrase'; // 'char' or 'phrase'
      let generatedDupStatsHTML = ''; 
      // ---------------------------------------------

```

**Find the `resetPage` function. Add these state resets:**

```javascript
      // Add inside resetPage() before mcc0update.style.display = 'none';
      globalCodeMap.clear();
      rawCharDefLines = [];
      generatedDupStatsHTML = '';
      document.getElementById('dashboardOverview').style.display = 'none';
      document.getElementById('downloadContainer').style.display = 'none';
      document.getElementById('modeSelect').disabled = false;
      document.getElementById('modeSelect').value = 'auto';

```

**Find `handleFile(event)`. Replace the reader.onload block:**

```javascript
        reader.onload = async function (e) {
          filecontent = e.target.result;
          await runAnalyzeFileAndDashboard(); // Execute analyze logic first
          processContent(); // Then pass to phrase processing
        };

```

**Insert this new function `runAnalyzeFileAndDashboard` anywhere in the script:**

```javascript
      async function runAnalyzeFileAndDashboard() {
        const lines = filecontent.split('\n').map(l => l.replace(/\r$/, ''));
        
        let inChardef = false;
        let charDefStartIndex = -1;
        let charDefEndIndex = lines.length;
        let zStartCount = 0;
        let yinmoCount = 0;
        let chiPuncCount = 0;
        let engSymCount = 0;

        globalCodeMap.clear();
        rawCharDefLines = [];

        // Parse metrics and build code map
        for (let i = 0; i < lines.length; i++) {
          const line = lines[i].trim();
          if (line === '%chardef begin') {
            inChardef = true;
            charDefStartIndex = i;
            continue;
          }
          if (!inChardef) continue;
          if (line === '%chardef end') {
            charDefEndIndex = i;
            break;
          }

          rawCharDefLines.push(line);
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts.length > 1 ? parts[1].trim() : '';

          if (code) {
            // Collect code map for Char mode & Stats
            if (char && /^[a-z]{1,4}$/.test(code)) {
              if (!globalCodeMap.has(code)) globalCodeMap.set(code, []);
              globalCodeMap.get(code).push(char);
            }

            // Dashboard conditions
            if (code.startsWith('z')) zStartCount++;
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) yinmoCount++;
            if (code.startsWith('x') && code[1] !== 'z' && code.length >= 2) chiPuncCount++;
            if (code.startsWith('xz')) engSymCount++;
          }
        }

        // Calculate duplicate stats logic (from domu1.html)
        const sortedCodeMap = new Map([...globalCodeMap.entries()].sort());
        const dupStats = new Map();
        for (const chars of sortedCodeMap.values()) {
          const dupCount = chars.length - 1; // 1 char = 0 dup, 2 chars = 1 dup...
          if (dupCount >= 0) dupStats.set(dupCount, (dupStats.get(dupCount) || 0) + 1);
        }

        const lenStats = [new Map(), new Map(), new Map(), new Map()];
        for (const [code, chars] of sortedCodeMap) {
          const lenIdx = Math.min(code.length - 1, 3);
          const dupCount = chars.length - 1;
          if (dupCount >= 0) {
            const map = lenStats[lenIdx];
            map.set(dupCount, (map.get(dupCount) || 0) + 1);
          }
        }

        // Build HTML for Stats Tab
        let statsHtml = `<div class="dup-stats-container">`;
        
        // Total Dup Stats
        statsHtml += `<div class="dup-box"><h3>總重碼統計 (Total Dup Stats)</h3><div class="dup-row">`;
        const maxDup = Math.max(...dupStats.keys(), 0);
        for (let i = 0; i <= maxDup; i++) {
          statsHtml += `<span class="dup-tag"><strong>${i}重:</strong> ${dupStats.get(i) || 0}</span>`;
        }
        statsHtml += `</div></div>`;

        // Category Dup Stats
        statsHtml += `<div class="dup-box"><h3>分類重碼統計 (Category Dup Stats)</h3>`;
        lenStats.forEach((map, i) => {
          const maxDupForLen = Math.max(...map.keys(), 0);
          statsHtml += `<div style="margin-bottom:8px; font-size:14px; font-weight:bold;">碼長 ${i + 1}:</div><div class="dup-row" style="margin-bottom:15px;">`;
          for (let j = 0; j <= maxDupForLen; j++) {
            statsHtml += `<span class="dup-tag"><strong>${j}重:</strong> ${map.get(j) || 0}</span>`;
          }
          statsHtml += `</div>`;
        });
        statsHtml += `</div>`;

        // Dup & Dedup Results TextAreas
        const collapseLines = [];
        const gzhahaLines = [];
        const outOfBoundsLines = [];

        for (const [code, chars] of sortedCodeMap) {
          if (chars.length > 1) collapseLines.push(`${code}\t${chars.join('')}`);
          if (chars.length <= 1) continue;
          
          const len = code.length;
          chars.forEach((char, idx) => {
            if (idx === 0) return;
            if (len === 1 || len === 2) {
              const suffix = ['z', 'zz', 'zx', 'xz'][idx - 1] || '';
              if (suffix) gzhahaLines.push(`${code}${suffix}\t${char}`);
              else outOfBoundsLines.push(`${code}\t${char}`);
            } else if (len === 3) {
              if (idx === 1) gzhahaLines.push(`${code}z\t${char}`);
              else if (idx === 2) gzhahaLines.push(`${code.slice(0, 2)}z${code.slice(2)}\t${char}`);
              else if (idx === 3) gzhahaLines.push(`${code.slice(0, 1)}z${code.slice(1)}\t${char}`);
              else outOfBoundsLines.push(`${code}\t${char}`);
            } else if (len === 4) {
              outOfBoundsLines.push(`${code}\t${char}`);
            }
          });
        }

        statsHtml += `<div class="dup-box"><h3>重碼結果 (${collapseLines.length})</h3><textarea class="modern-textarea" readonly>${collapseLines.join('\n')}</textarea></div>`;
        statsHtml += `<div class="dup-box"><h3>去重結果 (${gzhahaLines.length})</h3><textarea class="modern-textarea" readonly>${gzhahaLines.join('\n')}</textarea></div>`;
        statsHtml += `<div class="dup-box"><h3>超出字合集 (${outOfBoundsLines.length})</h3><textarea class="modern-textarea" readonly>${outOfBoundsLines.join('\n')}</textarea></div>`;
        statsHtml += `</div>`;
        
        generatedDupStatsHTML = statsHtml;

        // Pass calculated metrics to global scope for processContent to use
        window._parsedMetrics = { charDefLinesLen: rawCharDefLines.length, yinmoCount, chiPuncCount, engSymCount, zStartCount };
      }

      function showDupStatsSection(btnElement) {
        document.querySelectorAll('#statsBtnContainer .stats-btn').forEach(btn => btn.classList.remove('active'));
        if (btnElement) btnElement.classList.add('active');
        document.getElementById('statsDisplayContainer').innerHTML = generatedDupStatsHTML;
      }

```

**Find `processContent` and modify the dashboard updates:**
Replace the standard `document.getElementById('output').innerHTML = ...` injections with:

```javascript
        // Inside processContent, replace the first document.getElementById('output') with:
        document.getElementById('dashboardOverview').style.display = 'grid';
        if (window._parsedMetrics) {
          document.getElementById('dashCharDef').textContent = window._parsedMetrics.charDefLinesLen;
          document.getElementById('dashYinmo').textContent = window._parsedMetrics.yinmoCount;
          document.getElementById('dashChiPunc').textContent = window._parsedMetrics.chiPuncCount;
          document.getElementById('dashEngSym').textContent = window._parsedMetrics.engSymCount;
        }

        // ... lower down, replace the second document.getElementById('output') line with:
        document.getElementById('dashPhraseCount').textContent = mcc0Lines.length;
        if (window._parsedMetrics) {
          document.getElementById('dashPhraseDedup').textContent = window._parsedMetrics.zStartCount - mcc0Lines.length;
        }

```

**Find the download link generation in `processContent`:**

```javascript
        // Replace the download injection:
        document.getElementById('download').innerHTML =
          `<a href="${url}" download="haha_${timestamp}.txt">下載文件 (${size} bytes)</a>`;

```

**With:**

```javascript
        document.getElementById('downloadContainer').style.display = 'block';
        document.getElementById('downloadContainer').innerHTML =
          `<a href="${url}" download="haha_${timestamp}.txt" class="modern-download-btn">下載碼表 Download File (${size} bytes)</a>`;

```

**Find `handleEditSubmit()` and replace it with this mode-aware version:**

```javascript
      function handleEditSubmit() {
        let val = document.getElementById('codeInput').value.trim();
        if (!val) return;

        let selectedMode = document.getElementById('modeSelect').value;
        const isChinese = /[\u4e00-\u9fa5]/.test(val);

        // Auto logic assignment
        if (selectedMode === 'auto') {
          if ((!isChinese && !val.toLowerCase().startsWith('z')) || (isChinese && val.length === 1)) {
            selectedMode = 'char';
          } else {
            selectedMode = 'phrase';
          }
          document.getElementById('modeSelect').value = selectedMode;
        }
        
        // Lock the mode
        document.getElementById('modeSelect').disabled = true;
        activeMode = selectedMode;
        document.getElementById('codeInput').value = '';
        selectedEditItem = null;
        justMovedItem = null;
        pendingAction = null;

        if (activeMode === 'char') {
          // Initialize char mode state tracking
          if (undoStack.length === 0) {
            undoStack.push({ state: JSON.stringify(rawCharDefLines), desc: 'Baseline Char State', mode: 'char' });
          }
          currentEditCode = val.toLowerCase(); 
          // Extract base code if a character was entered
          if (isChinese) {
             let foundCode = null;
             for (const [code, chars] of globalCodeMap) {
               if (chars.includes(val)) { foundCode = code; break; }
             }
             if (foundCode) {
               // Extract prefix for displaying the sequence
               currentEditCode = foundCode[0];
             }
          }
          
          document.getElementById('inputArea').style.display = 'block';
          document.getElementById('editArea').style.display = 'block';
          document.getElementById('btnMoveEnd').style.display = 'none'; // Hide in char mode
          document.getElementById('btnDelete').style.display = 'none';  // Hide in char mode
          renderCharEditArea(currentEditCode, val);
          return;
        }

        // --- Original Phrase Mode Logic Follows ---
        document.getElementById('btnMoveEnd').style.display = 'inline-block';
        document.getElementById('btnDelete').style.display = 'inline-block';
        // ... (Keep existing code from here down to renderEditArea() for phrase mode logic)
        
        if (/^[a-zA-Z]+$/.test(val)) {
          if (val[0].toLowerCase() !== 'z') val = 'z' + val;
          val = val.substring(0, 4).toLowerCase();
          currentEditCode = val;
          currentTargetWord = '';
        } else {
          currentTargetWord = val;
          let lines = mcc0string.split('\n').filter(l => l.trim() !== '');

          if (!lines.includes(val)) {
            if (!originalSnapshotState || originalSnapshotState.size === 0) {
              originalSnapshotState = getSystemState();
            }
            saveStateForUndo(`Added new word: ${val}`);
            lines.push(val);
            processContent(JSON.stringify(lines));
          }

          let code = 'z';
          let c1 = ccharfirst.get(val[0]) || '';
          if (val.length === 1) {
            code += c1;
          } else if (val.length === 2) {
            let c2 = ccharfirst.get(val[1]) || '';
            let c3 = ccharsecond.get(val[1]) || 'x';
            code += c1 + c2 + c3;
          } else {
            let c2 = ccharfirst.get(val[1]) || '';
            let c3 = ccharfirst.get(val[2]) || '';
            code += c1 + c2 + c3;
          }
          currentEditCode = code.substring(0, 4);
        }

        if (currentEditCode.length >= 1) activeEditKeys.add('z');
        if (currentEditCode.length >= 2) activeEditKeys.add(currentEditCode.substring(0, 2));
        if (currentEditCode.length >= 3) activeEditKeys.add(currentEditCode.substring(0, 3));
        if (currentEditCode.length >= 4) activeEditKeys.add(currentEditCode);

        if (!originalSnapshotState || originalSnapshotState.size === 0) {
          originalSnapshotState = getSystemState();
        }

        document.getElementById('inputArea').style.display = 'block';
        document.getElementById('editArea').style.display = 'block';
        renderEditArea();
      }

```

**Insert this rendering logic for Char Mode anywhere:**

```javascript
      function renderCharEditArea(baseCode, targetInput) {
        let activeContainer = document.getElementById('editRows');
        let originalContainer = document.getElementById('originalRows');
        activeContainer.innerHTML = '';
        originalContainer.innerHTML = '';

        // Derive sequences (j, jj, jjj, jjjj) from the first character of the code
        const firstLetter = baseCode[0];
        const targetCodes = [firstLetter, firstLetter.repeat(2), firstLetter.repeat(3), firstLetter.repeat(4)];

        targetCodes.forEach(codeKey => {
          let rowDiv = document.createElement('div');
          rowDiv.className = 'edit-row';
          
          let title = document.createElement('div');
          title.textContent = codeKey;
          title.style.fontWeight = 'bold';
          title.style.marginBottom = '8px';
          if (codeKey === targetInput) title.className = 'highlighted-title'; // Highlight if exact code match
          rowDiv.appendChild(title);

          const charsForCode = globalCodeMap.get(codeKey) || [];
          
          if (charsForCode.length === 0) {
             // Show empty drop zone
             let emptyBtn = document.createElement('span');
             emptyBtn.className = 'empty-slot';
             emptyBtn.textContent = '+';
             emptyBtn.onclick = () => handleCharClick('+', codeKey);
             rowDiv.appendChild(emptyBtn);
          } else {
             charsForCode.forEach((char, idx) => {
               let span = document.createElement('span');
               span.textContent = char;
               span.className = `edit-item`;
               if (char === selectedEditItem) span.classList.add('selected');
               if (char === justMovedItem) span.classList.add('just-moved');
               
               // Target character highlighting
               if (char === targetInput) {
                 span.classList.add('target-word');
                 title.className = 'highlighted-title'; // Highlight title if row contains target char
               }

               span.onclick = () => handleCharClick(char, codeKey);
               rowDiv.appendChild(span);
             });
             
             // Always append an empty slot at the end of populated rows for trailing insertion
             let emptyBtn = document.createElement('span');
             emptyBtn.className = 'empty-slot';
             emptyBtn.textContent = '+';
             emptyBtn.onclick = () => handleCharClick('+', codeKey);
             rowDiv.appendChild(emptyBtn);
          }

          activeContainer.appendChild(rowDiv);
        });
      }

      function handleCharClick(clickedChar, targetCode) {
        if (!selectedEditItem) {
          if (clickedChar === '+') return; // Cannot select empty slot
          selectedEditItem = clickedChar;
          justMovedItem = null;
          renderCharEditArea(currentEditCode[0], ''); // Rerender to show selection
          return;
        }

        // Swap logic
        if (selectedEditItem === clickedChar) {
          selectedEditItem = null;
          renderCharEditArea(currentEditCode[0], '');
          return;
        }

        // Save State
        undoStack.push({ state: JSON.stringify(rawCharDefLines), desc: `${selectedEditItem} -> ${targetCode}`, mode: 'char' });
        redoStack = [];
        renderActionLogs();

        // Perform swap/move in raw lines array
        let sourceLineIdx = rawCharDefLines.findIndex(l => l.endsWith('\t' + selectedEditItem));
        if (sourceLineIdx !== -1) {
          rawCharDefLines.splice(sourceLineIdx, 1); // remove from old position
        }

        let newRowString = `${targetCode}\t${selectedEditItem}`;
        if (clickedChar === '+') {
           // Insert at the end of the block for that code, or just append
           let lastIdx = -1;
           for(let i = rawCharDefLines.length - 1; i >= 0; i--) {
             if (rawCharDefLines[i].startsWith(targetCode + '\t')) { lastIdx = i; break; }
           }
           if (lastIdx !== -1) rawCharDefLines.splice(lastIdx + 1, 0, newRowString);
           else rawCharDefLines.push(newRowString);
        } else {
           // Insert relative to clicked item
           let targetLineIdx = rawCharDefLines.findIndex(l => l === `${targetCode}\t${clickedChar}`);
           if (targetLineIdx !== -1) {
             if (sourceLineIdx > targetLineIdx) {
               rawCharDefLines.splice(targetLineIdx, 0, newRowString); // Insert before
             } else {
               rawCharDefLines.splice(targetLineIdx + 1, 0, newRowString); // Insert after
             }
           } else {
             rawCharDefLines.push(newRowString);
           }
        }

        justMovedItem = selectedEditItem;
        selectedEditItem = null;
        
        // Rebuild filecontent structure and re-process to cascade updates
        rebuildAndProcessFromCharLines();
      }

      function rebuildAndProcessFromCharLines() {
         const lines = filecontent.split('\n').map(l => l.replace(/\r$/, ''));
         const start = lines.indexOf('%chardef begin');
         const end = lines.indexOf('%chardef end');
         
         if (start !== -1 && end !== -1) {
             const newFileContentArr = [
                ...lines.slice(0, start + 1),
                ...rawCharDefLines,
                ...lines.slice(end)
             ];
             filecontent = newFileContentArr.join('\n');
             runAnalyzeFileAndDashboard(); 
             processContent(); // Refreshes UI, generates new download
             renderCharEditArea(currentEditCode[0], ''); 
         }
      }

```

**Find `handleUndo()` and `handleRedo()` and add mode branching:**

```javascript
      function handleUndo() {
        if (undoStack.length === 0) return;
        let prevState = undoStack.pop();
        
        if (prevState.mode === 'char') {
          redoStack.push({ state: JSON.stringify(rawCharDefLines), desc: `Undo: ${prevState.desc}`, mode: 'char' });
          rawCharDefLines = JSON.parse(prevState.state);
          rebuildAndProcessFromCharLines();
        } else {
          // Original phrase logic
          let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
          redoStack.push({ lines: currentLines, desc: `Undo: ${prevState.desc}`, mode: 'phrase' });
          let oldState = getSystemState();
          processContent(JSON.stringify(prevState.lines));
          detectAndRenderChanges(oldState, getSystemState());
        }
        justMovedItem = null; 
        renderActionLogs();
      }

      function handleRedo() {
        if (redoStack.length === 0) return;
        let nextState = redoStack.pop();
        
        if (nextState.mode === 'char') {
          undoStack.push({ state: JSON.stringify(rawCharDefLines), desc: nextState.desc, mode: 'char' });
          rawCharDefLines = JSON.parse(nextState.state);
          rebuildAndProcessFromCharLines();
        } else {
          let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
          undoStack.push({ lines: currentLines, desc: nextState.desc, mode: 'phrase' });
          let oldState = getSystemState();
          processContent(JSON.stringify(nextState.lines));
          detectAndRenderChanges(oldState, getSystemState());
        }
        justMovedItem = null; 
        renderActionLogs();
      }

```

**Find `handleEditCloseOnly()` and ensure unlock logic is present:**

```javascript
      function handleEditCloseOnly() {
        document.getElementById('inputArea').style.display = 'block';
        document.getElementById('editArea').style.display = 'none';
        document.getElementById('codeInput').value = '';
        document.getElementById('modeSelect').disabled = false; // Unlock dropdown
        // ... keep rest of handleEditCloseOnly

```

**Find `switchTab(tab)`. Add the default Stats redirect logic:**

```javascript
        if (tab === 'stats') {
          // Determine which filter button is active or fallback to Dup Stats default
          const activeBtn =
            document.querySelector('#statsBtnContainer .stats-btn.active') || document.getElementById('btnDupStats');
          if (activeBtn) {
            if (activeBtn.id === 'btnDupStats') { showDupStatsSection(activeBtn); }
            else { activeBtn.click(); }
          }
        }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5701
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

帖子 ejsoon »

參照domu1.js中的analyzeFile和processContent兩個函式,改進domchar5744.html:
一,當文本載入時,先執行analyzeFile函式(不用生成download),之後再把結果交給processContent函式。

二,修正overviewPanel統計數據:Yinmo Deduplication(編碼開頭不是z或x,後面含有z的行數),中文標點(編碼開頭為x但第二個字母不為z的行數),英文符號(編距開頭為xz的行數),詞組數量(mcc0string行數,這個是對的),詞組去重(所有編碼以z開頭的行數減去mcc0string行數)。

三,在Submit右邊加上「自動、單字、詞語」的切換按鈕,默認為自動。

當處於自動模式時,如果在codeInput所輸入的是英文字母且開頭不是z,或者輸入的是一個漢字,則自動判斷是單字模式;如果輸入的是英文字母且開頭是z,或者輸入的是兩個以上的漢字,則自動判斷是詞語模式。

當按下enter或點擊Submit後,模式切換按鈕切到「單字」或「詞語」,這時模式將不能再改變,除非按下btnCloseEdit。

四,當處於單字模式時,如果codeInput輸入的是一個漢字,則展示這個字所在的序列的重碼情況。

例如輸入「轟」(或者jjjj),則下方將顯示:
j 都十
jj 幹艹卄廾
jjj 轉
jjjj 轟

當點擊其中一個字A,它將會高亮,點擊另一個字B,如果A在B的前面,則A將插入至B的後面;如果A在B的後面,則A將插入至B的前面。之後執行analyzeFile函式,統計數據更新,並生成download。

這些操作都將計入undo-redo,reset,操作log中。

如果codeInput輸入的是一個編碼,則這個編碼作為標題將高亮。

當一個字從一個欄目移到另一個欄目,則它的編碼將變為所在欄的標題的編碼。

當一個編碼本身沒有字(空碼),或者一開始有字,但是後來被移走了,則它下方(原本有字的地方)將出現一個用方框圍住的加號,用以其它單字再移過去。

單字模式沒有「put to the end」和「delete」。其餘跟詞語模式類似。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

代码: 全选

參照domu1.js中的analyzeFile和processContent兩個函式,改進domchar5744.html:
一,當文本載入時,先執行analyzeFile函式(不用生成download),之後再把結果交給processContent函式。

二,修正overviewPanel統計數據:Yinmo Deduplication(編碼開頭不是z或x,後面含有z的行數),中文標點(編碼開頭為x但第二個字母不為z的行數),英文符號(編距開頭為xz的行數),詞組數量(mcc0string行數,這個是對的),詞組去重(所有編碼以z開頭的行數減去mcc0string行數)。

三,在Submit右邊加上「自動、單字、詞語」的切換按鈕,默認為自動。

當處於自動模式時,如果在codeInput所輸入的是英文字母且開頭不是z,或者輸入的是一個漢字,則自動判斷是單字模式;如果輸入的是英文字母且開頭是z,或者輸入的是兩個以上的漢字,則自動判斷是詞語模式。

當按下enter或點擊Submit後,模式切換按鈕切到「單字」或「詞語」,這時模式將不能再改變,除非按下btnCloseEdit。

四,當處於單字模式時,如果codeInput輸入的是一個漢字,則展示這個字所在的序列的重碼情況。

例如輸入「轟」(或者jjjj),則下方將顯示:
j	都十
jj	幹艹卄廾
jjj	轉
jjjj	轟

當點擊其中一個字A,它將會高亮,點擊另一個字B,如果A在B的前面,則A將插入至B的後面;如果A在B的後面,則A將插入至B的前面。之後執行analyzeFile函式,統計數據更新,並生成download。

這些操作都將計入undo-redo,reset,操作log中。

如果codeInput輸入的是一個編碼,則這個編碼作為標題將高亮。

當一個字從一個欄目移到另一個欄目,則它的編碼將變為所在欄的標題的編碼。

當一個編碼本身沒有字(空碼),或者一開始有字,但是後來被移走了,則它下方(原本有字的地方)將出現一個用方框圍住的加號,用以其它單字再移過去。

單字模式沒有「put to the end」和「delete」。其餘跟詞語模式類似。

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

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

帖子 ejsoon »

gemini pro要休息到4:52:

代码: 全选

Here are the specific modifications required to implement the requested features. Please replace the corresponding sections in your `domchar5744.html` file.

All logic, variables, and comments have been refactored in English according to your instructions.

### 1. Add the Mode Toggle to the UI

Locate the `<div id="inputArea">` block and insert the `<select id="modeSelect">` right before the Submit button.

**Replace this block:**

```html
        <div id="inputArea">
          <label for="codeInput" style="font-weight: bold">Encode Input: </label>
          <input type="text" id="codeInput" placeholder="Enter code (e.g. zhy) or word" />
          <button id="submitCode" onclick="handleEditSubmit()">Submit</button>
        </div>

```

**With this:**

```html
        <div id="inputArea">
          <label for="codeInput" style="font-weight: bold">Encode Input: </label>
          <input type="text" id="codeInput" placeholder="Enter code (e.g. zhy) or word" />
          <select id="modeSelect" style="padding: 8px; border-radius: 4px; margin-right: 6px; font-size: 14px;">
            <option value="auto">Auto</option>
            <option value="single">Single Word</option>
            <option value="phrase">Phrase</option>
          </select>
          <button id="submitCode" onclick="handleEditSubmit()">Submit</button>
        </div>

```

---

### 2. File Loading Logic Chain (Task 1)

Chain the execution so `runDomu1LogicAndGetIntegratedText` (acting as `analyzeFile`) runs first, then triggers `processContent()`.

**Replace this block:**

```javascript
      function handleFile(event) {
        const file = event.target.files[0];
        if (!file) return;

        document.getElementById('selectcinfile').classList.add('hide');

        const reader = new FileReader();
        reader.onload = function (e) {
          filecontent = e.target.result;

          // Execute domu1.html logic first, which processes stats and modifies raw text
          filecontent = runDomu1LogicAndGetIntegratedText(filecontent);

          // Execute domchar5741.html logic
          processContent();
        };
        reader.readAsText(file);
      }

```

**With this:**

```javascript
      function handleFile(event) {
        const file = event.target.files[0];
        if (!file) return;

        document.getElementById('selectcinfile').classList.add('hide');

        const reader = new FileReader();
        reader.onload = function (e) {
          filecontent = e.target.result;

          // Task 1: First execute analyzeFile logic (does not trigger download directly)
          // It modifies the content with dedup stats and bounds, returning integrated text
          filecontent = runDomu1LogicAndGetIntegratedText(filecontent);

          // Task 1: Pass the integrated result to processContent
          processContent();
        };
        reader.readAsText(file);
      }

```

---

### 3. Overview Panel Statistics Accuracy (Task 2)

Ensure the counting rules strictly match the requested parameters exactly inside the iteration loops.

**Replace this block inside `processContent()`:**

```javascript
        charDefLines.forEach(line => {
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts[1];
          if (code && char) {
            cchardefine.set(char, code);
            statSingleWord++;
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) statYinmo++;
            if (code.startsWith('x') && code[1] !== 'z') statChiPunc++;
            if (code.startsWith('xz')) statEngSym++;
            if (code.startsWith('z')) allZLineCount++;
          }
        });

```

**With this:**

```javascript
        charDefLines.forEach(line => {
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts[1];
          if (code && char) {
            cchardefine.set(char, code);
            statSingleWord++;
            
            // Task 2: Yinmo Deduplication -> Code does not start with z or x, but contains z afterwards
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) {
               statYinmo++;
            }
            
            // Task 2: Chinese Punctuation -> Code starts with x but the second letter is not z
            if (code.startsWith('x') && code.length > 1 && code[1] !== 'z') {
               statChiPunc++;
            }
            
            // Task 2: English Symbol -> Code starts exactly with xz prefix
            if (code.startsWith('xz')) {
               statEngSym++;
            }
            
            // Task 2 count for Deduplication Baseline
            if (code.startsWith('z')) {
               allZLineCount++;
            }
          }
        });

```

---

### 4. Submit & Mode Toggle Auto-Detection (Task 3 & 4)

Adjust input handling to auto-detect the user's intent, lock the UI, and generate accurate single-word structural display sequences.

**Replace the `handleEditSubmit()` function:**

```javascript
      function handleEditSubmit() {
        let val = document.getElementById('codeInput').value.trim();
        if (!val) return;
        document.getElementById('codeInput').value = '';

        selectedEditItem = null;
        justMovedItem = null;
        pendingAction = null;
        updateActionButtonsUI();

        let isEnglishCode = /^[a-zA-Z]+$/.test(val);

        // Detect Single Word Mode (English without 'z' prefix, or a single Chinese char)
        isSingleWordMode = false;
        singleWordActiveCode = '';
        currentTargetWord = '';

        if (isEnglishCode && val[0].toLowerCase() !== 'z') {
          isSingleWordMode = true;
          singleWordActiveCode = val.toLowerCase().substring(0, 4);
        } else if (!isEnglishCode && val.length === 1) {
          isSingleWordMode = true;
          currentTargetWord = val; // Set the target word to highlight the Chinese char
          // Reverse lookup code from charDefCodeMap
          for (let [code, chars] of charDefCodeMap) {
            if (chars.includes(val)) {
              singleWordActiveCode = code;
              break;
            }
          }
        }

        if (isSingleWordMode) {
          activeEditKeys.clear(); // Only show sequences for this code
          for (let i = 1; i <= singleWordActiveCode.length; i++) {
            activeEditKeys.add(singleWordActiveCode.substring(0, i));
          }
        } else {
        // ... (rest of old logic up to renderEditArea) ...

```

**With this:**

```javascript
      function handleEditSubmit() {
        let val = document.getElementById('codeInput').value.trim();
        if (!val) return;
        document.getElementById('codeInput').value = '';

        selectedEditItem = null;
        justMovedItem = null;
        pendingAction = null;

        let modeSelect = document.getElementById('modeSelect');
        let currentMode = modeSelect.value;
        let isEnglishCode = /^[a-zA-Z]+$/.test(val);

        // Task 3: Auto-detect if "auto" is selected
        if (currentMode === 'auto') {
          if ((isEnglishCode && val[0].toLowerCase() !== 'z') || (!isEnglishCode && val.length === 1)) {
            currentMode = 'single';
          } else {
            currentMode = 'phrase';
          }
        }

        // Lock the mode UI
        modeSelect.value = currentMode;
        modeSelect.disabled = true;

        isSingleWordMode = (currentMode === 'single');
        singleWordActiveCode = '';
        currentTargetWord = '';

        if (isSingleWordMode) {
          if (!isEnglishCode && val.length === 1) {
            currentTargetWord = val; // Highlight target char
            for (let [code, chars] of charDefCodeMap) {
              if (chars.includes(val)) {
                singleWordActiveCode = code;
                break;
              }
            }
          } else {
            // Target code as title if english code
            singleWordActiveCode = val.toLowerCase().substring(0, 4);
          }

          // Generate sequential lookup: j, jj, jjj, jjjj
          activeEditKeys.clear();
          let baseCode = singleWordActiveCode || 'x'; // safety fallback
          for (let i = 1; i <= baseCode.length; i++) {
            activeEditKeys.add(baseCode.substring(0, i));
          }
          
          // Task 4: Hide End & Delete Buttons
          document.getElementById('btnMoveEnd').style.display = 'none';
          document.getElementById('btnDelete').style.display = 'none';
        } else {
          // Enable phrase-specific UI buttons
          document.getElementById('btnMoveEnd').style.display = 'inline-block';
          document.getElementById('btnDelete').style.display = 'inline-block';
          
          currentTargetWord = val;
          if (isEnglishCode) {
            if (val[0].toLowerCase() !== 'z') val = 'z' + val;
            val = val.substring(0, 4).toLowerCase();
            currentEditCode = val;
            currentTargetWord = '';
          } else {
            let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
            if (!lines.includes(val)) {
              if (!originalSnapshotState || originalSnapshotState.size === 0) {
                originalSnapshotState = getSystemState();
              }
              saveStateForUndo(`Added new word: ${val}`);
              lines.push(val);
              processContent(JSON.stringify(lines));
            }

            let code = 'z';
            let c1 = ccharfirst.get(val[0]) || '';
            if (val.length === 1) {
              code += c1;
            } else if (val.length === 2) {
              let c2 = ccharfirst.get(val[1]) || '';
              let c3 = ccharsecond.get(val[1]) || 'x';
              code += c1 + c2 + c3;
            } else {
              let c2 = ccharfirst.get(val[1]) || '';
              let c3 = ccharfirst.get(val[2]) || '';
              code += c1 + c2 + c3;
            }
            currentEditCode = code.substring(0, 4);
          }
          if (currentEditCode.length >= 1) activeEditKeys.add('z');
          if (currentEditCode.length >= 2) activeEditKeys.add(currentEditCode.substring(0, 2));
          if (currentEditCode.length >= 3) activeEditKeys.add(currentEditCode.substring(0, 3));
          if (currentEditCode.length >= 4) activeEditKeys.add(currentEditCode);

          if (!originalSnapshotState || originalSnapshotState.size === 0) {
            originalSnapshotState = getSystemState();
          }
        }

        updateActionButtonsUI();
        document.getElementById('inputArea').style.display = 'block';
        document.getElementById('editArea').style.display = 'block';
        renderEditArea();
      }

```

---

### 5. Restore Auto Mode on Close (Task 3)

Ensure the lock releases when editing is closed.

**Inside `handleEditCloseOnly()`:**

```javascript
      function handleEditCloseOnly() {
        document.getElementById('inputArea').style.display = 'block';
        document.getElementById('editArea').style.display = 'none';
        document.getElementById('codeInput').value = '';
        
        // Add this line to unlock mode UI
        document.getElementById('modeSelect').disabled = false;
        // Optionally revert to auto
        document.getElementById('modeSelect').value = 'auto'; 
        
        // ... (keep the rest identical)

```

---

### 6. Rendering the Empty Target Blocks `[+]` (Task 4)

Update `renderEditArea` to ensure empty slots still draw, and bind click actions passing the code constraint.

**Locate this condition in `renderEditArea()`:**

```javascript
          if (activeItems.length > 0 || originalItems.length > 0) {

```

**Replace it and its child logic block with this:**

```javascript
          // Task 4: Force render on single word mode to allow empty slot UI
          if (isSingleWordMode || activeItems.length > 0 || originalItems.length > 0) {
            let isIdentical = false;
            let activeWordsArr = activeItems.map(i => i.w);
            if (activeWordsArr.length === originalItems.length && activeWordsArr.every((v, i) => v === originalItems[i])) {
              isIdentical = true;
            }

            let activeRowDiv = document.createElement('div');
            activeRowDiv.className = 'edit-row';

            if (isIdentical && originalItems.length > 0) {
              activeRowDiv.style.backgroundColor = '#e8f5e9';
            }

            let activeTitle = document.createElement('div');
            activeTitle.textContent = label;
            activeTitle.style.fontWeight = 'bold';
            activeTitle.style.marginBottom = '8px';

            if (isSingleWordMode && singleWordActiveCode === k && !currentTargetWord) {
              activeTitle.className = 'target-code-title';
            }
            activeRowDiv.appendChild(activeTitle);

            activeItems.forEach(item => {
              let span = document.createElement('span');
              span.textContent = item.w;
              span.className = `edit-item ${item.cls}`;

              // Adjusted selected style targeting for objects in Single Mode
              if (isSingleWordMode) {
                 if (selectedEditItem && selectedEditItem.word === item.w && selectedEditItem.code === k) span.classList.add('selected');
              } else {
                 if (item.w === selectedEditItem) span.classList.add('selected');
              }
              
              if (item.w === justMovedItem) span.classList.add('just-moved'); 
              if (item.w === currentTargetWord) span.classList.add('target-word');
              if (changedWords.has(item.w)) span.classList.add('changed');

              span.onclick = () => handleItemClick(item.w, k); // Pass row code
              activeRowDiv.appendChild(span);
            });

            // Task 4: Render empty slot box target
            if (isSingleWordMode) {
              let addBtn = document.createElement('span');
              addBtn.textContent = '[+]';
              addBtn.className = 'edit-item empty-slot-btn';
              addBtn.style.border = '1px solid #aaa'; // Framed box
              addBtn.style.color = '#777';
              addBtn.style.fontFamily = 'monospace';
              if (selectedEditItem && selectedEditItem.word === '[+]' && selectedEditItem.code === k) addBtn.classList.add('selected');
              addBtn.onclick = () => handleItemClick('[+]', k);
              activeRowDiv.appendChild(addBtn);
            }

            // ... (keep the rest of the original/mobile preview logic identical)

```

---

### 7. Refactoring the Item Click and Directional Movement System (Task 4)

Replace `handleItemClick` entirely. This new version calculates structural placement indices, injects logic to re-trigger `analyzeFile`, update stats, and rebuild the text file in memory immediately upon movement completion.

**Replace the entire `handleItemClick(word)` function with this block:**

```javascript
      // Helper function to rebuild the file content after a single-word mode movement
      function rebuildFileContentAndRefresh() {
        let linesFile = filecontent.split('\n');
        let chardefBeginIdx = linesFile.indexOf('%chardef begin');
        let chardefEndIdx = -1;
        let inDef = false;

        for (let i = 0; i < linesFile.length; i++) {
          if (linesFile[i] === '%chardef begin') {
            inDef = true;
            continue;
          }
          if (!inDef) continue;
          if (linesFile[i] === '%chardef end' || linesFile[i].startsWith('x') || /[^z]z/.test(linesFile[i].split('\t')[0])) {
            chardefEndIdx = i;
            break;
          }
        }

        if (chardefBeginIdx > -1 && chardefEndIdx > -1) {
          let newBlock = [];
          for (let [c, chars] of charDefCodeMap) {
            chars.forEach(ch => newBlock.push(`${c}\t${ch}`));
          }
          linesFile.splice(chardefBeginIdx + 1, chardefEndIdx - chardefBeginIdx - 1, ...newBlock);
          filecontent = linesFile.join('\n');

          // Task 4: Re-trigger analyze logic and generate final download stats
          filecontent = runDomu1LogicAndGetIntegratedText(filecontent);
          
          let oldState = getSystemState();
          processContent();
          let newState = getSystemState();
          detectAndRenderChanges(oldState, newState);
        }
      }

      function handleItemClick(word, code = null) {
        if (pendingAction) {
          applyActionToWord(pendingAction, word);
          pendingAction = null;
          selectedEditItem = null;
          updateActionButtonsUI();
          return;
        }

        if (isSingleWordMode) {
          // --- SINGLE WORD MODE LOGIC (A -> B Relative Movement) ---
          if (!selectedEditItem) {
            selectedEditItem = { word, code };
            justMovedItem = null;
            renderEditArea();
            return;
          }

          let A = selectedEditItem;
          let B = { word, code };

          // Clicking the same target cancels selection
          if (A.word === B.word && A.code === B.code) {
            selectedEditItem = null;
            renderEditArea();
            return;
          }

          let arrA = charDefCodeMap.get(A.code) || [];
          let arrB = charDefCodeMap.get(B.code) || [];
          let idxA = arrA.indexOf(A.word);
          let idxB = B.word === '[+]' ? -1 : arrB.indexOf(B.word);

          if (idxA > -1) {
            let logMsg = B.word === '[+]' ? `${A.word} moved to empty slot ${B.code}` : `${A.word} moved relative to ${B.word}`;
            saveStateForUndo(logMsg);

            arrA.splice(idxA, 1); // Extract A from memory

            // Task 4 Logic: If A is before B -> A inserts after B. If A is after B -> A inserts before B.
            let isABeforeB = false;
            if (A.code.length < B.code.length) isABeforeB = true; // Different structural rows
            else if (A.code === B.code && idxA < idxB) isABeforeB = true; // Same row

            let targetArr = (A.code === B.code) ? arrA : arrB;
            let newIdxB = B.word === '[+]' ? targetArr.length : targetArr.indexOf(B.word);

            if (B.word === '[+]') {
              targetArr.push(A.word);
            } else {
              if (isABeforeB) {
                targetArr.splice(newIdxB + 1, 0, A.word); // Insert After
              } else {
                targetArr.splice(newIdxB, 0, A.word); // Insert Before
              }
            }

            // Task 4: Move code properties dynamically across bounds
            if (A.code !== B.code) {
               charDefCodeMap.set(A.code, arrA);
               charDefCodeMap.set(B.code, targetArr);
            }

            justMovedItem = A.word;
            selectedEditItem = null;
            rebuildFileContentAndRefresh();
            renderEditArea();
          }
        } else {
          // --- PHRASE MODE LOGIC (Original Baseline Logic) ---
          if (!selectedEditItem) {
            selectedEditItem = word;
            justMovedItem = null;
            renderEditArea();
          } else {
            if (selectedEditItem === word) {
              selectedEditItem = null;
              renderEditArea();
              return;
            }

            let oldState = getSystemState();
            let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
            let originalIdxA = lines.indexOf(selectedEditItem);
            let originalIdxB = lines.indexOf(word);

            if (originalIdxA > -1 && originalIdxB > -1 && originalIdxA !== originalIdxB) {
              let placement = originalIdxA > originalIdxB ? 'before' : 'after';
              saveStateForUndo(`${selectedEditItem} -> ${placement} ${word}`);

              lines.splice(originalIdxA, 1);
              let newIdxB = lines.indexOf(word);

              if (originalIdxA > originalIdxB) {
                lines.splice(newIdxB, 0, selectedEditItem);
              } else {
                lines.splice(newIdxB + 1, 0, selectedEditItem);
              }

              justMovedItem = selectedEditItem;
              processContent(JSON.stringify(lines));
              let newState = getSystemState();
              detectAndRenderChanges(oldState, newState);
            }
            selectedEditItem = null;
          }
        }
      }

```

---

Let me know if you need to trace any specific code mapping behavior further, or if you plan to extend the duplicate stats behavior visually next!
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5701
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

帖子 ejsoon »

gemini flash來吧:

代码: 全选

// --- 保持原代碼邏輯不變的單字前半區塊 ---
        let charDefStart = lines.indexOf('%chardef begin') + 1;
        let charDefEnd = lines.findIndex((line, i) => i > charDefStart && line.includes('z'));
        if (charDefEnd === -1) charDefEnd = lines.length;

        const charDefLines = lines.slice(charDefStart, charDefEnd);

        // 【新增邏輯】在不動原邏輯前提下,獲取首個含 z 的行到 %chardef end 的完整殘餘區塊
        let realCharDefEndLine = lines.indexOf('%chardef end');
        if (realCharDefEndLine === -1) realCharDefEndLine = lines.length;
        
        // 擷取從 charDefEnd(第一個含z的行)開始,到真正的 %chardef end 為止的行
        const remainingCharDefLines = lines.slice(charDefEnd, realCharDefEndLine);

        // 1. 先遍歷前半段(原代碼完全不動)
        charDefLines.forEach(line => {
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts[1];
          if (code && char) {
            cchardefine.set(char, code);
            statSingleWord++;
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) statYinmo++;
            if (code.startsWith('x') && code[1] !== 'z') statChiPunc++;
            if (code.startsWith('xz')) statEngSym++;
            if (code.startsWith('z')) allZLineCount++;
          }
        });

        // 2. 【新增邏輯】對另一個變數(殘餘區塊)進行逐行統計與補算,邏輯與上面完全一致
        remainingCharDefLines.forEach(line => {
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts[1];
          if (code && char) {
            cchardefine.set(char, code);
            statSingleWord++;
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) statYinmo++;
            if (code.startsWith('x') && code[1] !== 'z') statChiPunc++;
            if (code.startsWith('xz')) statEngSym++;
            if (code.startsWith('z')) allZLineCount++;
          }
        });
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5701
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

帖子 ejsoon »

代码: 全选

// ==========================================
          // 修正:依據指定條件精準定位起始點與終點並替換
          // ==========================================
          
          let startIdx = -1;     // 起始點:第一個含 z 但非 z 開頭的行
          let insertIdx = -1;    // 終點:第一個出現 x 開頭的行

          // 1. 尋找起始點 (按照 domu1.js 的 /[^z]z/ 邏輯)
          for (let i = chardefBeginIdx + 1; i < integratedLines.length; i++) {
            if (integratedLines[i] === '%chardef end') break;
            
            const code = integratedLines[i].split('\t')[0];
            if (code && /[^z]z/.test(code)) {
              startIdx = i;
              break;
            }
          }

          // 2. 尋找終點 (第一個出現 x 開頭的行)
          for (let i = chardefBeginIdx + 1; i < integratedLines.length; i++) {
            if (integratedLines[i] === '%chardef end') {
              insertIdx = i;
              break;
            }
            
            const code = integratedLines[i].split('\t')[0];
            if (code && code.startsWith('x')) {
              insertIdx = i;
              break;
            }
          }

          // 如果沒找到符合 /[^z]z/ 的起始點,則預設將起始點設為終點 (退化為純插入)
          if (startIdx === -1) {
            startIdx = (insertIdx !== -1) ? insertIdx : integratedLines.length;
          }

          // 如果終點沒找到,則預設到檔案末尾
          if (insertIdx === -1) {
            insertIdx = integratedLines.length;
          }

          // 3. 計算舊資料行數並執行精準「替換」
          if (gzhahaLines.length > 0) {
            // 舊資料的行數 = 終點行號 - 起始點行號
            let deleteCount = Math.max(0, insertIdx - startIdx);
            
            // 從起始點開始,刪除 deleteCount 行舊數據,並填入新的 gzhahaLines
            integratedLines.splice(startIdx, deleteCount, ...gzhahaLines);
          }
        }

        return integratedLines.join('\n');
      }
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

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