若要擷取 title = "Blue = Green" 這類指派,請一併比對鍵和完整的引號值。本篇 Mac 操作指南使用 Safari 中的 JavaScript,在回報格式錯誤的行時保留其中的等號、引號和反斜線。

為什麼用等號分割會遺失值?

請看這個合成指派:

title = "Blue = Green"

預期的鍵是 title;值(包含原有引號)是 "Blue = Green"。只有第一個等號用來分隔欄位。第二個等號屬於引號內的文字。

JavaScript 的 split("=") 會在每次出現分隔符號時切開字串,並不會解讀引號。加入限制(例如 split("=", 2))只會截短結果陣列,不會保留第一個分隔符號之後的所有內容。這符合標準中對 String.prototype.split 的定義。

正確的方法取決於輸入的語法。修改文字前,請先參考以下判斷指南:

輸入條件 合適的方法
每行一個指派;只需分隔欄位 找出第一個等號,並保留其後的所有內容。
每行一個指派;也必須檢查引號和鍵的語法 使用下方定義的指派模式比對完整的一行。
每行有多個指派,或值跨越多行 使用能處理這些額外規則的解析器。

在此任務中,驗證完整的一行還能多做一項檢查:若引號未配對或有意外的尾隨文字,程式會回報錯誤,而不會產生不完整的擷取結果。

這個範例會保留哪些指派語法?

下方程式實作一種刻意設計得很精簡的文字格式。以下是本範例定義的規則,不代表它能解析所有 JavaScript 指派:

  • 每個非空白行包含一個指派。
  • 鍵以 ASCII 字母或底線開頭,後接 ASCII 字母、數字或底線。
  • 值使用成對的直式單引號或直式雙引號。空的引號值有效。
  • 在值內,反斜線會與下一個字元配對,但若下一個字元是回車或換行則例外。兩個字元都會原樣保留。
  • 鍵、分隔符號和值的前後可以有空格或定位字元。引號內的空格和定位字元會保留為值的一部分。
  • 實際換行可使用 LF、CRLF 或 CR。引號值不可跨越這些換行。
  • 註解、分號和未加引號的值不屬於此語法範圍。

例如,caption='Small = calm' 有效。note="Use \"a=b\" here" 也有效:經跳脫的雙引號屬於值的一部分。相較之下,caption='Small = calm" 的引號不成對,必須修正。

輸出會將每個值儲存為 rawValue,包括外圍引號。擷取時不會解碼跳脫序列:字元 \n 仍是兩個字元。如此一來,結果可用於重新排列指派文字,同時保留其書寫形式。

重複的鍵也會依原始順序保留為不同項目。這樣可避免同一個鍵再次出現時,前一個值遭到悄悄取代。

如何在 Mac 的 Safari 中執行轉換?

Safari 的 Web Inspector 提供執行 JavaScript 的位置。請依序前往 Safari → 設定⋯ → 進階 → 顯示網頁開發者功能,啟用此功能。接著選擇 開發 → 顯示網頁檢查器,或按下 Option-Command-I。WebKit 的啟用 Web Inspector文件說明了這些步驟。

選擇 主控台 分頁,並在輸入提示中執行下方完整程式。WebKit 在 Web Inspector 介面參考中,將此提示說明為用來評估 JavaScript 的輸入欄位。此範例只會處理程式內撰寫的字串。

範例使用 String.raw,讓反斜線在建構 JavaScript 字串時保持原樣。編輯範例時,請記得反引號和 ${...} 仍具有樣板字串的特殊意義;此輸入並未包含這些字元。若要加入這些字元,請先參閱 JavaScript String.raw() 文件。

(() => {
  const source = String.raw`title = "Blue = Green"
caption='Small = calm'
note = "Use \"a=b\" here"
marker = "$1 = $$"
empty = ""`;

  const assignment = /^[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*("(?:\\[^\r\n]|[^"\\\r\n])*"|'(?:\\[^\r\n]|[^'\\\r\n])*')[ \t]*$/;

  function extractAssignments(text) {
    const pairs = [];
    const errors = [];
    const lines = text.split(/\r\n|\n|\r/);

    lines.forEach((line, index) => {
      if (/^[ \t]*$/.test(line)) return;
      const match = assignment.exec(line);
      if (!match || match[0].length !== line.length) {
        errors.push(`Line ${index + 1}: expected key = quoted value`);
        return;
      }
      pairs.push({ key: match[1], rawValue: match[2] });
    });

    if (errors.length) throw new Error(errors.join("\n"));
    return pairs;
  }

  const pairs = extractAssignments(source);
  const extracted = pairs
    .map(({ key, rawValue }) => `key: ${key}\nvalue: ${rawValue}`)
    .join("\n\n");
  console.log(extracted);

  const normalized = source.split(/\r\n|\n|\r/)
    .map(line => line.replace(assignment,
      (_whole, key, rawValue) => key + " = " + rawValue))
    .join("\n");
  console.log("Normalized assignments:\n" + normalized);
})();

這是一個包含範例資料的完整程式,不需要匯入或額外套件。外層函式會將宣告限制在區域範圍內,因此修改範例資料後可以重新執行整段程式。

第一則主控台訊息會分隔欄位:

key: title
value: "Blue = Green"

key: caption
value: 'Small = calm'

key: note
value: "Use \"a=b\" here"

key: marker
value: "$1 = $$"

key: empty
value: ""

第二則訊息會以一致的間距呈現指派:

標準化後的指派:
title = "Blue = Green"
caption = 'Small = calm'
note = "Use \"a=b\" here"
marker = "$1 = $$"
empty = ""

選取並複製適合桌面任務的文字區塊即可。程式透過 console.log 顯示結果;WebKit 在其 主控台物件 API中有相關說明。程式不會寫入檔案。

此模式如何保留引號內的等號?

此模式有兩個擷取群組:一個用來擷取鍵,另一個用來擷取完整的引號值。模式內的 (?:...) 群組用於整理各種選項,不會建立額外的擷取欄位。這種擷取群組與非擷取群組的用法,符合 JavaScript 群組文件中的說明。

比對鍵之後,模式會讀取一次指派分隔符號,接著選擇雙引號或單引號分支。每個分支都接受一般字元(包括等號),或由反斜線與下一個字元組成的配對。未經跳脫且與開頭相符的引號會結束該分支。

此結構能處理經跳脫的引號,不會誤將它們當成值的結尾,也能處理成對的反斜線:消耗一對反斜線後,後續未經跳脫的引號便可結束字串。引號內容可以重複零次,因此也支援空值。

程式透過 match[1] 和 match[2] 讀取擷取結果。exec() 呼叫失敗時會傳回 null,如 ECMAScript 的 RegExp.prototype.exec所規定。額外的長度比對則要求比對結果涵蓋整個實際行。

只有空格和定位字元會被視為前後格式。程式絕不會修剪值的內部內容。因此,label = " Blue = Green " 會保留引號內容兩端的空格。

重新格式化時,為什麼使用取代函式?

標準化步驟會傳回擷取到的鍵、格式統一的分隔符號,以及擷取到的值。取代回呼會先收到完整比對結果,接著才是兩個擷取值。JavaScript 的 String.prototype.replace()文件說明了此參數順序。

回呼傳回的文字會直接插入,不會解讀 $1 或 $$ 等取代代碼,因此範例值 "$1 = $$" 能保持原樣。相同文件也解釋了取代字串與取代函式之間的差異。

固定的取代樣板(例如 "$1 = $2")也適用於此模式:擷取到的內容會直接插入,不會再次進行替換。動態組合取代文字時,回呼尤其實用,因為它能讓傳回值與取代樣板語法保持分開。

標準化會刻意變更前後空格,並將實際換行轉換為 LF,同時保留空白行和完全一致的引號值文字。若任務要求保留原始空格和換行,請用擷取出的鍵值組進行檢查,並保留原始的 source 不變。

使用結果前應檢查哪些事項?

擷取函式會收集附有行號的錯誤,並在顯示任一結果前擲回錯誤。只含空格或定位字元的空白行會明確略過。因此,格式錯誤的非空白行不會從結果中消失,造成部分擷取結果看似成功。

請用以下合成案例診斷意外輸入:

指派文字 預期結果
x="a=b=c" 保留完整的引號值。
x='' 接受空的單引號值。
x=" a=b " 保留引號內的空格。
x="a\"=b" 保留經跳脫的引號和等號。
x="a=b 回報缺少結尾引號。
x="a" extra 回報尾隨文字。
x="a" y="b" 拒絕同一行有多個指派。
x=a=b 依此語法拒絕未加引號的值。

擷取與標準化邏輯已在 Node.js 20.19.2 中搭配所示範例執行。另有十五項確定性檢查,涵蓋引號內的等號、空值、經跳脫的引號、成對的反斜線、字面美元符號代碼、格式錯誤的行、重複鍵和空白行處理。這驗證了本例所撰寫的轉換邏輯,但不表示曾在 Safari 介面中實際測試。

若某行比對失敗,先檢查引號是否成對,再檢查是否有未配對的末尾反斜線、尾隨文字或不支援的鍵。若任務需要更多語法,請明確修改語法規則。對於本文所述的每行一個指派格式,結果會是一組可重複使用的鍵和值,且引號值中的所有內部等號都會保留。

來源