若要擷取 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 介面中實際測試。
若某行比對失敗,先檢查引號是否成對,再檢查是否有未配對的末尾反斜線、尾隨文字或不支援的鍵。若任務需要更多語法,請明確修改語法規則。對於本文所述的每行一個指派格式,結果會是一組可重複使用的鍵和值,且引號值中的所有內部等號都會保留。
來源
- 啟用 Web Inspector — WebKit。
- Web Inspector 介面 — WebKit。
- 主控台物件 API — WebKit。
- ECMAScript® 2027 語言規範 — 文字處理 — Ecma International / TC39,持續更新中的草案。
- String.raw() — MDN Web Docs。
- 群組與反向參照 — MDN Web Docs。
- String.prototype.replace() — MDN Web Docs。