若要套用替換對照表而不變更新插入的文字,只搜尋原始字串並建立獨立結果即可。本指南使用 Mac 上 Safari 的 JavaScript,以文字字面值進行可預期的替換,來更新一小段文字。
假設桌面編輯工作要求將 blue 改為 green,並將 green 改為 gold。原始的 blue 應只變更一次,成為 green。原始的 green 應變成 gold。以下做法可處理這種差異、重疊片語、引號文字,以及含有標點符號的替換值。
為什麼替換迴圈會讓文字變更兩次?
將每條規則套用到前一條規則的輸出上,就會形成連鎖替換。套用這兩條顏色規則時,第一個操作會插入 green;第二個操作接著會找到這個新插入的字詞,並將它改成 gold。重新排列規則或許能修正某個例子,但無法為每種對照表訂定可靠的行為。
JavaScript 的 replace() 會傳回新字串,並保持輸入不變。之後的呼叫若使用傳回的字串,就會發生連鎖替換。因此,這項工作需要一條明確規則:每個符合項目都必須來自原始輸入。請參閱 MDN 的 String.prototype.replace() 文件。
以編寫好的輸入 The blue cup is beside the green cup. 為例,預期結果是 The green cup is beside the gold cup. 原始的兩個顏色字詞都會變更,而插入的 green 則保持不變。以下實作執行的就是這項轉換。
替換鍵重疊時應如何處理?
避免連鎖替換後,還有另一項決定要做:多個鍵符合相同來源文字時,該由哪個鍵優先?在這項編輯工作中,採用以下明確規則。這些是此範例的設計選擇,並非 JavaScript 自動提供的規則。
- 從左到右掃描原始輸入。
- 在每個位置,選擇符合的最長鍵。
- 用完整的來源符合項目向後推進,再繼續掃描。
- 直接插入對應值,不再搜尋該值。
- 原樣複製未符合的文字。
- 嚴格比對大小寫與空格。
例如,如果 blue 和 blue vase 都是鍵,則在相同起始位置,片語 blue vase 優先。較早開始的符合項目,優先於較晚開始的較長符合項目。一旦某段來源文字已被取用,重疊的符合項目便不能重複使用它。
這項規則適合少量的文字字面片語編輯:具體片語優先於較短的前綴。如此一來,輸出也不會受到輸入鍵的長度順序影響。如果你的工作需要採用不同的重疊規則,請先決定好再套用對照表。
如何在 Mac 上執行這項轉換?
Safari 提供 JavaScript 主控台,因此此範例不需要安裝其他應用程式。透過 Safari → 設定⋯ → 進階 → 顯示網頁開發者功能,啟用 Web Inspector。接著開啟 開發 → 顯示 Web Inspector,或按下 Option-Command-I。這些步驟依據 WebKit 的 Web Inspector 設定文件。
在 Safari 網址列輸入 about:blank 開啟空白頁面,接著開啟該頁面的檢閱器並選取 Console 分頁。將下方完整程式貼到主控台提示字元,然後執行。WebKit 說明,提示字元會評估 JavaScript,並在主控台中顯示結果。請參閱 Web Inspector 介面。
此範例是完整的瀏覽器程式,使用合成文字,且不需匯入任何內容。外層函式會將宣告保留在區域範圍內,因此你可以編輯範例後,再次貼上並執行整個程式區塊。
(() => {
function applyReplacementMap(input, replacements) {
if (typeof input !== "string" || !(replacements instanceof Map)) {
throw new TypeError("Use a string and a Map.");
}
const entries = [...replacements];
for (const [key, value] of entries) {
if (
typeof key !== "string" ||
key.length === 0 ||
typeof value !== "string"
) {
throw new TypeError(
"Keys must be nonempty strings; values must be strings."
);
}
}
entries.sort((a, b) => b[0].length - a[0].length);
const output = [];
let position = 0;
while (position < input.length) {
const entry = entries.find(([key]) =>
input.startsWith(key, position)
);
if (entry) {
output.push(entry[1]);
position += entry[0].length;
} else {
output.push(input.slice(position, position + 1));
position += 1;
}
}
return output.join("");
}
const replacements = new Map([
["blue", "green"],
["green", "gold"],
["blue vase", "green bowl"],
["[tag]", "$&"],
["(draft)", ""]
]);
const input =
'The blue vase is beside the green cup. Label: "[tag]". Note: (draft)';
const result = applyReplacementMap(input, replacements);
console.log(result);
})();
記錄的文字如下所示。Note: 後面保留了一個尾端空格,因為規則只會移除 (draft),前面的空格會保留下來。
The green bowl is beside the gold cup. Label: "$&". Note:
若要調整範例,請編輯 input,以及 new Map(...) 中的配對。每個配對都包含完全符合的來源字串及其替換內容。每個鍵只使用一筆項目:JavaScript 的 Map 會儲存唯一鍵,因此重複的鍵會更新其值,而非建立另一條互相競爭的規則。請參閱 MDN 的 Map 參考資料。
為什麼插入的文字會保持不變
名為 position 的游標只會在 input 中向前移動。比對會使用 input.startsWith(key, position),它會檢查指定位置是否有字串,且區分大小寫;詳情請參閱 MDN 的 startsWith() 參考資料。輸出陣列絕不會作為搜尋目標。
沒有規則符合時,slice(position, position + 1) 會複製下一個來源單位。第二個切片索引不包含在範圍內,而切片不會修改原始字串。這些有文件說明的語意,讓函式能在獨立建立結果的同時,保留未符合的內容。請參閱 MDN 的 slice() 參考資料。
此範例會以 green bowl 替換 blue vase。雖然插入的片語也包含另一個鍵 green,但函式已經越過原始片語,不會檢查 green bowl。若輸入稍後還有原始的 green,它仍會變成 gold。
文字字面值與刪除的處理方式
這項實作會直接附加替換值。因此,$& 會保持為 $&,而鍵中的方括號或括號也都是一般字元。若替換字串為空,則會刪除符合的來源文字區段。此函式不會使用正規表示式,也不會將內容解讀為替換樣板。
空鍵會遭到拒絕,因為它不會消耗任何來源文字;若允許空鍵,符合分支便無法向前推進。非字串值也會在轉換開始前遭到拒絕,讓錯誤的值能夠明確呈現,而不是被默默轉換或略過。空對照表會原樣傳回輸入;空輸入則會傳回空字串。
複製結果前應如何檢查?
使用能清楚區分預期行為的小型案例。以下預期結果是根據上方的實作推導而得;除非另有說明,皆使用其中的範例對照表。
| 檢查項目 | 輸入 | 預期結果 |
|---|---|---|
| 插入的文字保持不變 | blue green |
green gold |
| 較長片語優先 | blue vase |
green bowl |
| 替換內容中的標點符號保留為文字 | [tag] |
$& |
| 比對會區分大小寫 | Blue |
Blue |
| 比對包含子字串 | blueprint |
greenprint |
| 引號保留 | \"blue\" |
\"green\" |
| 空對照表 | blue |
blue |
替換函式及以上行為已在 Node.js 20.19.2 中檢查,並一併檢查了刪除、空輸入、重疊符合項目及無效對照表的拒絕處理。這驗證了該 JavaScript 轉換在此執行環境中的行為;Safari 介面步驟則依據 WebKit 的文件。
請特別留意子字串檢查。此函式不會推斷字詞邊界,也不會修正文法。鍵可能會在較長字詞中符合,而移除文字也可能留下多餘空格。若要編輯片語,請檢查周圍字元,並讓對照表具體到符合預期的變更。
也請區分單次套用與重複套用。在原始文字上執行一次函式,可避免該次執行期間發生連鎖替換。若將結果再次輸入函式,就會開始新的轉換,因此先前插入的字詞可能會符合。重新執行編輯時,請保留原始的 input 作為來源。
一次 JavaScript replace 呼叫能做到相同的事嗎?
可以。對於一小組固定且簡單的鍵,使用一個全域正規表示式和替換回呼,就能以相同方式避免連鎖替換。ECMAScript 規格會從來源字串收集符合項目,然後建立替換結果。請參閱規定的正規表示式替換演算法。
以下是處理兩個固定顏色鍵的獨立完整範例:
(() => {
const replacements = new Map([
["blue", "green"],
["green", "gold"]
]);
const input = "The blue cup is beside the green cup.";
const result = input.replace(
/blue|green/g,
(word) => replacements.get(word)
);
console.log(result);
// The green cup is beside the gold cup.
})();
g 旗標會讓運算式替換所有符合項目。從回呼傳回值也能避免 $& 等替換字串樣式,因為 $& 在其他情況下代表符合的子字串。MDN 在 replace() 參考資料中明確說明了這兩種行為。
若要處理包含標點符號且會變動的對照表,建構正規表示式會增加額外要求。鍵必須逸出為文字字面模式,重疊的選項也需要明確的優先順序。JavaScript 提供 RegExp.escape(),可用於建構文字字面模式,但 MDN 指出較舊的瀏覽器可能不支援它。請參閱 MDN 的 RegExp.escape() 文件。
對於本文的桌面片語編輯工作,文字字面掃描器會清楚呈現這些決定:它會讀取原始文字,在每個位置選擇最長的鍵,並原樣附加每個替換值。對原始段落執行程式、檢查結果的措辭和空格,再將完成的文字複製到文件中。
來源
- 啟用 Web Inspector — WebKit。
- Web Inspector 介面 — WebKit。
- String.prototype.replace() — MDN Web Docs,Mozilla。
- Map — MDN Web Docs,Mozilla。
- String.prototype.startsWith() — MDN Web Docs,Mozilla。
- String.prototype.slice() — MDN Web Docs,Mozilla。
- ECMAScript 語言規範:RegExp.prototype [ %Symbol.replace% ] — Ecma International,TC39。
- RegExp.escape() — MDN Web Docs,Mozilla。