Чтобы применить карту замен, не изменяя только что вставленный текст, ищите совпадения только в исходной строке и формируйте результат отдельно. В этом руководстве используется JavaScript в Safari на Mac, чтобы обновить небольшой фрагмент с помощью буквальных и предсказуемых замен.
Предположим, задача по редактированию текста на компьютере требует заменить 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, поэтому для этого примера не нужно устанавливать дополнительные приложения. Включите Web Inspector через Safari → Settings… → Advanced → Show features for web developers. Затем откройте Develop → Show Web Inspector или нажмите Option-Command-I. Эти шаги соответствуют инструкции WebKit по настройке Web Inspector.
Введите about:blank в адресную строку Safari, чтобы открыть пустую страницу, затем откройте инспектор и выберите вкладку 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 ключи уникальны, поэтому повторное добавление ключа обновляет его значение, а не создает еще одно конкурирующее правило. См. справочник MDN по Map.
Почему вставленный текст остается без изменений
Курсор position перемещается только по input. Для поиска используется input.startsWith(key, position), который проверяет наличие строки в заданной позиции и учитывает регистр, как указано в справочнике MDN по startsWith(). Массив результатов никогда не используется для поиска.
Если ни одно правило не подходит, slice(position, position + 1) копирует следующую единицу исходного текста. Второй индекс среза не включается в результат, а сам срез не изменяет исходную строку. Благодаря этим документированным свойствам функция сохраняет неизмененный текст, одновременно формируя отдельный результат. См. справочник MDN по slice().
В примере вместо blue vase вставляется green bowl. Хотя эта вставленная фраза содержит другой ключ, 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.