Um eine Ersetzungstabelle anzuwenden, ohne neu eingefügten Text zu verändern, durchsuche nur die ursprüngliche Zeichenfolge und erstelle ein separates Ergebnis. Diese Anleitung verwendet JavaScript in Safari auf einem Mac, um eine kurze Passage mit wörtlichen, vorhersehbaren Ersetzungen zu aktualisieren.

Angenommen, bei einer Desktop-Bearbeitung sollen blue durch green und green durch gold ersetzt werden. Ein ursprüngliches blue soll genau einmal zu green werden. Ein ursprüngliches green soll zu gold werden. Die folgende Vorgehensweise berücksichtigt diesen Unterschied, sich überschneidende Phrasen, zitierte Textstellen und Ersetzungswerte mit Satzzeichen.

Warum verändert eine Ersetzungsschleife Text zweimal?

Eine Schleife, die jede Regel auf die Ausgabe der vorherigen Regel anwendet, erzeugt eine Kaskade. Bei diesen beiden Farberegeln fügt der erste Vorgang green ein; der zweite Vorgang findet dann dieses neu eingefügte Wort und ändert es zu gold. Eine andere Reihenfolge der Regeln löst vielleicht ein Beispiel, legt aber kein verlässliches Verhalten für jede Tabelle fest.

JavaScripts replace() gibt eine neue Zeichenfolge zurück und lässt die Eingabe unverändert. Eine Kaskade entsteht, wenn nachfolgende Aufrufe die zurückgegebene Zeichenfolge erhalten. Die Aufgabe braucht deshalb eine eindeutige Regel: Jeder Treffer muss aus der ursprünglichen Eingabe stammen. Siehe die Dokumentation zu String.prototype.replace() auf MDN.

Für die formulierte Eingabe The blue cup is beside the green cup. lautet das gewünschte Ergebnis The green cup is beside the gold cup. Beide ursprünglichen Farbwörter werden geändert, während das eingefügte green unverändert bleibt. Diese Umwandlung führt die folgende Implementierung aus.

Was soll passieren, wenn sich Ersetzungsschlüssel überschneiden?

Wenn Kaskaden verhindert werden, bleibt eine weitere Entscheidung: Welcher Schlüssel gilt, wenn mehrere Schlüssel auf denselben Quelltext passen? Für diese Bearbeitungsaufgabe gilt die folgende ausdrückliche Regelung. Das sind Gestaltungsentscheidungen für das Beispiel und keine automatisch von JavaScript vorgegebenen Regeln.

  • Durchsuche die ursprüngliche Eingabe von links nach rechts.
  • Wähle an jeder Position den längsten passenden Schlüssel.
  • Verbrauche den gesamten passenden Quelltext, bevor du fortfährst.
  • Füge den zugeordneten Wert unverändert ein, ohne ihn zu durchsuchen.
  • Kopiere nicht passende Textstellen exakt.
  • Berücksichtige Groß- und Kleinschreibung sowie Leerzeichen exakt.

Wenn beispielsweise sowohl blue als auch blue vase Schlüssel sind, gewinnt die Phrase blue vase an derselben Startposition. Ein Treffer, der früher beginnt, hat Vorrang vor einem längeren Treffer, der später beginnt. Sobald ein Quelltextabschnitt verbraucht wurde, kann ein sich überschneidender Treffer ihn nicht erneut verwenden.

Diese Regelung eignet sich für eine kleine Menge wörtlicher Phrasenänderungen: Spezifische Phrasen haben Vorrang vor ihren kürzeren Präfixen. Außerdem ist die Ausgabe dadurch unabhängig von der Reihenfolge, in der Schlüssel unterschiedlicher Länge eingegeben wurden. Wenn deine Aufgabe eine andere Regelung für Überschneidungen erfordert, lege sie fest, bevor du die Tabelle anwendest.

Wie kannst du die Umwandlung auf einem Mac ausführen?

Safari bietet eine JavaScript-Konsole, daher muss für dieses Beispiel keine zusätzliche Anwendung installiert werden. Aktiviere den Web Inspector über Safari → Einstellungen… → Erweitert → Funktionen für Webentwickler anzeigen. Öffne dann Entwickler → Web-Inspector einblenden oder drücke Option-Command-I. Diese Schritte folgen der WebKit-Dokumentation zur Einrichtung des Web Inspectors.

Öffne eine leere Seite, indem du about:blank in das Adressfeld von Safari eingibst. Öffne dann den Inspektor und wähle den Tab Konsole aus. Füge das vollständige Programm unten in die Konsoleneingabe ein und führe es aus. WebKit dokumentiert, dass die Eingabe JavaScript ausführt und die Ergebnisse in der Konsole anzeigt. Siehe Web-Inspector-Oberfläche.

Das Beispiel ist ein vollständiges Browserprogramm mit künstlichem Text und ohne Importe. Seine äußere Funktion hält die Deklarationen lokal, sodass du den Block nach einer Änderung des Beispiels erneut einfügen und ausführen kannst.

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

Der ausgegebene Text ist unten zu sehen. Nach Note: bleibt ein abschließendes Leerzeichen, weil die Regel nur (draft) entfernt und das vorangehende Leerzeichen bewahrt.

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

Um das Beispiel anzupassen, ändere input und die Paare in new Map(...). Jedes Paar enthält zuerst die exakte Quellzeichenfolge und danach ihre Ersetzung. Verwende pro Schlüssel einen Eintrag: JavaScript-Maps enthalten eindeutige Schlüssel. Wird ein Schlüssel wiederholt, aktualisiert das seinen Wert, statt eine weitere konkurrierende Regel anzulegen. Siehe die Map-Referenz auf MDN.

Warum eingefügter Text unverändert bleibt

Der Cursor namens position rückt nur durch input vor. Die Suche verwendet input.startsWith(key, position), das an einer angegebenen Position eine Zeichenfolge prüft und Groß- und Kleinschreibung unterscheidet, wie in der Referenz zu startsWith() auf MDN dokumentiert. Das Ausgabe-Array wird nie als Suchziel verwendet.

Wenn keine Regel passt, kopiert slice(position, position + 1) die nächste Quelleinheit. Der zweite Slice-Index ist exklusiv, und das Slicing verändert die ursprüngliche Zeichenfolge nicht. Dank dieser dokumentierten Eigenschaften kann die Funktion nicht passende Inhalte bewahren und zugleich ihr Ergebnis separat aufbauen. Siehe die Referenz zu slice() auf MDN.

Das Beispiel fügt für blue vase den Text green bowl ein. Obwohl diese eingefügte Phrase einen weiteren Schlüssel enthält, green, ist die Funktion bereits über die ursprüngliche Phrase hinausgegangen. Sie untersucht green bowl nie. Ein ursprüngliches green an späterer Stelle in der Eingabe wird weiterhin zu gold geändert.

Wie wörtliche Werte und Löschungen funktionieren

Diese Implementierung hängt Ersetzungswerte direkt an. Daher bleibt $& unverändert $&, und eckige Klammern oder runde Klammern in Schlüsseln sind gewöhnliche Zeichen. Eine leere Ersetzungszeichenfolge löscht den passenden Quelltextabschnitt. Diese Funktion verwendet weder reguläre Ausdrücke noch die Interpretation von Ersetzungsvorlagen.

Leere Schlüssel werden zurückgewiesen, weil sie keinen Quelltext verbrauchen; würde man einen solchen Schlüssel zulassen, könnte der passende Zweig nicht weiter vorrücken. Auch Werte, die keine Zeichenfolgen sind, werden vor Beginn der Umwandlung zurückgewiesen. So wird ein fehlerhafter Wert sichtbar, statt stillschweigend umgewandelt oder ausgelassen zu werden. Eine leere Map gibt die Eingabe unverändert zurück, und eine leere Eingabe ergibt eine leere Zeichenfolge.

Wie solltest du das Ergebnis prüfen, bevor du es kopierst?

Verwende kleine Fälle, die das gewünschte Verhalten deutlich machen. Die folgenden erwarteten Ergebnisse ergeben sich aus der obigen Implementierung. Sofern nicht anders angegeben, verwenden sie die Beispiel-Map.

Prüfung Eingabe Erwartetes Ergebnis
Eingefügter Text bleibt unverändert blue green green gold
Längere Phrase hat Vorrang blue vase green bowl
Satzzeichen in Ersetzung bleiben wörtlich erhalten [tag] $&
Groß- und Kleinschreibung wird berücksichtigt Blue Blue
Auch Teilzeichenfolgen werden berücksichtigt blueprint greenprint
Anführungszeichen bleiben erhalten "blue" "green"
Leere Map blue blue

Die Ersetzungsfunktion und dieses Verhalten wurden zusammen mit Löschungen, leerer Eingabe, sich überschneidenden Treffern und der Zurückweisung ungültiger Maps in Node.js 20.19.2 überprüft. Damit ist die JavaScript-Umwandlung in dieser Laufzeit verifiziert; die Schritte für die Safari-Oberfläche stammen aus der Dokumentation von WebKit.

Achte besonders auf die Prüfung mit Teilzeichenfolgen. Diese Funktion erkennt keine Wortgrenzen und korrigiert keine Grammatik. Ein Schlüssel kann innerhalb eines längeren Wortes passen, und durch das Entfernen von Text können überzählige Leerzeichen entstehen. Prüfe bei einer Bearbeitung von Phrasen die umgebenden Zeichen und formuliere die Tabelle so spezifisch, wie es die gewünschte Änderung erfordert.

Unterscheide außerdem zwischen einmaliger und wiederholter Anwendung. Wenn du die Funktion einmal auf den ursprünglichen Text anwendest, entstehen innerhalb dieses Durchlaufs keine Kaskaden. Wenn du ihr Ergebnis erneut als Eingabe gibst, beginnt eine neue Umwandlung, sodass zuvor eingefügte Wörter dann passen können. Verwende beim erneuten Ausführen einer Bearbeitung den ursprünglichen input als Quelle.

Kann ein einzelner JavaScript-Aufruf von replace dasselbe leisten?

Ja. Für eine kleine, feste Menge einfacher Schlüssel kann ein einzelner globaler regulärer Ausdruck mit einer Ersetzungsfunktion dasselbe kaskadenfreie Verhalten bieten. Die ECMAScript-Spezifikation sammelt Treffer in der Quellzeichenfolge und erstellt dann das Ersetzungsergebnis. Siehe den spezifizierten Algorithmus für reguläre Ausdrucksersetzungen.

Dieses separate, vollständige Beispiel verarbeitet die beiden festen Farbschlüssel:

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

Das Flag g sorgt dafür, dass der Ausdruck alle Treffer ersetzt. Die Rückgabe von Werten aus einer Funktion vermeidet außerdem Ersetzungszeichenfolgen-Muster wie $&, das andernfalls für die gefundene Teilzeichenfolge steht. MDN dokumentiert beide Verhaltensweisen ausdrücklich in der replace()-Referenz.

Bei einer veränderlichen Map mit Satzzeichen sind beim Erstellen eines regulären Ausdrucks zusätzliche Anforderungen zu berücksichtigen. Schlüssel müssen als wörtliche Muster maskiert werden, und für sich überschneidende Alternativen braucht es eine ausdrückliche Priorität. JavaScript stellt RegExp.escape() bereit, um wörtliche Muster zu erstellen; laut MDN wird es jedoch von älteren Browsern möglicherweise nicht unterstützt. Siehe die Dokumentation zu RegExp.escape() auf MDN.

Für die hier behandelte Bearbeitung von Phrasen auf dem Desktop macht der wörtliche Scanner diese Entscheidungen sichtbar: Er liest den ursprünglichen Text, wählt an jeder Position den längsten Schlüssel und hängt jede Ersetzung unverändert an. Führe ihn mit der ursprünglichen Passage aus, prüfe Wortlaut und Abstände des Ergebnisses und füge den fertigen Text dann in dein Dokument ein.

Quellen