Aby zastosować mapę zamian bez zmieniania nowo wstawionego tekstu, przeszukuj tylko oryginalny ciąg i buduj osobny wynik. W tym przewodniku używamy JavaScriptu w Safari na Macu, aby zaktualizować krótki fragment za pomocą dosłownych, przewidywalnych zamian.

Załóżmy, że zadanie edycji na komputerze wymaga zamiany blue na green oraz green na gold. Oryginalne blue powinno zostać zmienione na green jeden raz. Oryginalne green powinno zostać zmienione na gold. Poniższe podejście uwzględnia tę różnicę, nakładające się frazy, cytowany tekst oraz wartości zamian zawierające znaki interpunkcyjne.

Dlaczego pętla zamian zmienia tekst dwukrotnie?

Pętla, która stosuje każdą regułę do wyniku poprzedniej, powoduje kaskadę zmian. Przy tych dwóch regułach dotyczących kolorów pierwsza operacja wstawia green; druga operacja znajduje następnie to nowo wstawione słowo i zmienia je na gold. Zmiana kolejności reguł może rozwiązać jeden przykład, ale nie określa niezawodnego działania dla każdej mapy.

Metoda replace() w JavaScripcie zwraca nowy ciąg i nie zmienia danych wejściowych. Kaskada powstaje, gdy kolejne wywołania otrzymują zwrócony ciąg. Dlatego zadanie wymaga jasnej reguły: każde dopasowanie musi pochodzić z oryginalnych danych wejściowych. Zobacz dokumentację String.prototype.replace() w MDN.

Dla przygotowanego tekstu wejściowego The blue cup is beside the green cup. oczekiwany wynik to The green cup is beside the gold cup. Oba oryginalne słowa oznaczające kolory zostają zmienione, a wstawione green pozostaje bez zmian. Taką transformację wykonuje poniższa implementacja.

Co powinno się stać, gdy klucze zamian na siebie zachodzą?

Zapobieganie kaskadom pozostawia jeszcze jedną decyzję: który klucz wygrywa, gdy kilka pasuje do tego samego tekstu źródłowego? W tym zadaniu edycyjnym zastosuj następującą jawną regułę. To założenia przyjęte na potrzeby przykładu, a nie automatyczne zasady JavyScriptu.

  • Przeszukuj oryginalne dane wejściowe od lewej do prawej.
  • W każdej pozycji wybieraj najdłuższy pasujący klucz.
  • Przed kontynuowaniem zużyj całe dopasowanie źródłowe.
  • Wstawiaj przypisaną wartość dosłownie, bez jej przeszukiwania.
  • Kopiuj niedopasowany tekst dokładnie.
  • Uwzględniaj wielkość liter i odstępy.

Na przykład, jeśli kluczami są zarówno blue, jak i blue vase, w tej samej pozycji początkowej wygrywa fraza blue vase. Wcześniej rozpoczynające się dopasowanie ma pierwszeństwo przed dłuższym dopasowaniem zaczynającym się później. Po zużyciu fragmentu źródłowego nakładające się dopasowanie nie może go ponownie wykorzystać.

Ta reguła sprawdza się przy edycji niewielkiego zbioru dosłownych fraz: konkretne frazy mają pierwszeństwo przed ich krótszymi prefiksami. Dzięki niej wynik nie zależy też od kolejności wprowadzania kluczy o różnej długości. Jeśli zadanie wymaga innej reguły rozstrzygania nakładających się dopasowań, ustal ją przed zastosowaniem mapy.

Jak uruchomić transformację na Macu?

Safari udostępnia konsolę JavaScriptu, więc ten przykład nie wymaga instalowania dodatkowej aplikacji. Włącz inspektor WWW przez Safari → Ustawienia… → Zaawansowane → Pokaż funkcje dla programistów WWW. Następnie otwórz Programowanie → Pokaż inspektor WWW albo naciśnij Option-Command-I. Te kroki są zgodne z dokumentacją konfiguracji inspektora WWW WebKit.

Otwórz pustą stronę, wpisując about:blank w polu adresu Safari, a następnie otwórz jej inspektor i wybierz kartę Console. Wklej cały poniższy program do wiersza poleceń konsoli i uruchom go. Dokumentacja WebKit wyjaśnia, że wiersz ten wykonuje kod JavaScript i wyświetla jego wyniki w konsoli. Zobacz Interfejs inspektora WWW.

Przykład to kompletny program przeglądarkowy z syntetycznym tekstem i bez importów. Funkcja zewnętrzna ogranicza zasięg deklaracji, dzięki czemu możesz ponownie wkleić i uruchomić blok po edycji przykładu.

(() => {
  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);
})();

Zalogowany tekst pokazano poniżej. Po Note: pozostaje jedna spacja końcowa, ponieważ reguła usuwa tylko (draft), zachowując poprzedzającą spację.

The green bowl is beside the gold cup. Label: "$&". Note: 

Aby dostosować przykład, edytuj input oraz pary wewnątrz new Map(...). Każda para zawiera dokładny ciąg źródłowy, po którym następuje jego zamiennik. Użyj jednego wpisu na klucz: mapy w JavaScripcie przechowują unikatowe klucze, więc ponowne podanie klucza aktualizuje jego wartość, zamiast tworzyć kolejną konkurującą regułę. Zobacz opis Map w MDN.

Dlaczego wstawiony tekst pozostaje bez zmian

Kursor o nazwie position przesuwa się wyłącznie w obrębie input. Dopasowanie korzysta z input.startsWith(key, position), które sprawdza, czy ciąg występuje w określonej pozycji, i uwzględnia wielkość liter, zgodnie z dokumentacją w opisie startsWith() w MDN. Tablica wynikowa nigdy nie służy jako tekst do przeszukiwania.

Gdy żadna reguła nie pasuje, slice(position, position + 1) kopiuje kolejną jednostkę źródłową. Drugi indeks w funkcji slice jest wyłączny, a wycinanie nie modyfikuje oryginalnego ciągu. Te udokumentowane właściwości pozwalają funkcji zachować niedopasowany tekst i jednocześnie osobno budować wynik. Zobacz opis slice() w MDN.

Przykład wstawia green bowl w miejsce blue vase. Chociaż wstawiona fraza zawiera inny klucz, green, funkcja zdążyła już przejść dalej niż oryginalna fraza. Nie sprawdza green bowl. Późniejsze oryginalne green w tekście nadal zostaje zmienione na gold.

Jak działają dosłowne wartości i usuwanie

Ta implementacja bezpośrednio dopisuje wartości zamian. W rezultacie $& pozostaje $&, a nawiasy kwadratowe i okrągłe w kluczach są zwykłymi znakami. Pusty ciąg zamiany usuwa dopasowany fragment źródłowy. Ta funkcja nie używa wyrażeń regularnych ani interpretacji wzorców ciągu zamiany.

Puste klucze są odrzucane, ponieważ nie zużywają żadnego tekstu źródłowego; dopuszczenie takiego klucza uniemożliwiłoby przejście dalej po gałęzi dopasowania. Wartości niebędące ciągami znaków również są odrzucane przed rozpoczęciem transformacji, aby błędna wartość była widoczna, zamiast zostać po cichu przekonwertowana lub pominięta. Pusta mapa zwraca dane wejściowe bez zmian, a pusty tekst wejściowy zwraca pusty ciąg.

Jak sprawdzić wynik przed skopiowaniem?

Użyj małych przypadków, które pozwalają odróżnić oczekiwane działanie. Poniższe wyniki oczekiwane wynikają z powyższej implementacji. O ile nie zaznaczono inaczej, używają jej przykładowej mapy.

Sprawdzenie Dane wejściowe Oczekiwany wynik
Wstawiony tekst pozostaje bez zmian blue green green gold
Wygrywa dłuższa fraza blue vase green bowl
Interpunkcja w zamienniku pozostaje dosłowna [tag] $&
Dopasowanie uwzględnia wielkość liter Blue Blue
Dopasowanie obejmuje podciągi blueprint greenprint
Cudzysłowy pozostają "blue" "green"
Pusta mapa blue blue

Funkcję zamiany i opisane zachowania sprawdzono w Node.js 20.19.2, w tym usuwanie, pusty tekst wejściowy, nakładające się dopasowania i odrzucanie nieprawidłowej mapy. Weryfikuje to transformację JavaScriptu w tym środowisku; kroki dotyczące interfejsu Safari pochodzą z dokumentacji WebKit.

Zwróć szczególną uwagę na sprawdzenie podciągów. Ta funkcja nie wykrywa granic słów ani nie poprawia gramatyki. Klucz może pasować wewnątrz dłuższego słowa, a usunięcie tekstu może pozostawić dodatkowe spacje. W zadaniu edycji fraz sprawdź sąsiednie znaki i doprecyzuj mapę stosownie do zamierzonej zmiany.

Rozróżnij też jednorazowe zastosowanie od wielokrotnego. Jednokrotne uruchomienie funkcji na oryginalnym tekście zapobiega kaskadom w ramach tego uruchomienia. Ponowne przekazanie wyniku do funkcji rozpoczyna nową transformację, więc wcześniej wstawione słowa mogą wtedy pasować. Przy ponownym wykonywaniu edycji używaj oryginalnego input jako tekstu źródłowego.

Czy pojedyncze wywołanie JavaScriptowej metody replace może zrobić to samo?

Tak. W przypadku niewielkiego, stałego zbioru prostych kluczy jedno globalne wyrażenie regularne i funkcja zwrotna zamiany mogą zapewnić takie samo działanie bez kaskad. Specyfikacja ECMAScript zbiera dopasowania z ciągu źródłowego, a następnie tworzy wynik zamiany. Zobacz określony algorytm zamiany z użyciem wyrażeń regularnych.

Ten osobny, kompletny przykład obsługuje dwa stałe klucze kolorów:

(() => {
  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.
})();

Flaga g sprawia, że wyrażenie zastępuje wszystkie dopasowania. Zwracanie wartości z funkcji zwrotnej pozwala też uniknąć wzorców ciągu zamiany, takich jak $&, które w przeciwnym razie oznacza dopasowany podciąg. MDN wyraźnie opisuje oba zachowania w opisie replace().

W przypadku zmiennej mapy zawierającej znaki interpunkcyjne konstruowanie wyrażenia regularnego wiąże się z dodatkowymi wymaganiami. Klucze trzeba uciec, aby były dosłownymi wzorcami, a kolejność nakładających się wariantów wymaga jawnego określenia priorytetu. JavaScript udostępnia RegExp.escape() do tworzenia dosłownych wzorców, ale MDN zwraca uwagę, że w starszych przeglądarkach może go brakować. Zobacz dokumentację RegExp.escape() w MDN.

W opisanym tu zadaniu edycji fraz na komputerze skaner dosłowny wyraźnie pokazuje te decyzje: odczytuje oryginalny tekst, wybiera najdłuższy klucz w każdej pozycji i dopisuje każdy zamiennik bez zmian. Uruchom go na oryginalnym fragmencie, sprawdź brzmienie i odstępy w wyniku, a następnie skopiuj gotowy tekst do dokumentu.

Źródła