Whitespace Search&Replace

Die Anforderung, mehrere Leerzeichen zusammenzufassen, taucht immer wieder auf. Bei Suchen/Ersetzungen mit Leerräumen bzw. Whitespace ist allerdings besondere Vorsicht geboten.
Zwischen Leerräumen befinden sich oft Marker, die bei der Suche ignoriert werde. Diese werden dann bei der Ersetzung unbeabsichtigt gelöscht. Das Grundsätzliche Problem habe ich bereits in diesem Post zusammengefasst.

Bei der Suche mit GREP gibt es die Möglichkeit mit \s alle Leerräume inklusive der Umbruchzeichen zu finden. Ab InDesign CS4 empfiehlt es sich mit der Unicode-Zeichenklasse \p{Z*} nur noch die Leerräume zu suchen.

Wenn ausschließlich nach Leeräumen gesucht wird, werden die folgenden Zeichen gefunden (whitespace.indd):

Wenn jetzt mehrere dieser Zeichen hintereinander auftreten, und durch einen einzigen Leerraum ersetzt werden sollen. Entstehen meiner Ansicht nach zwei grundsätzliche Probleme:

  1. Ignorierte Zeichen wie z.B. Indexmarken oder XML-Tags, die innerhalb des Suchergebnisses stehen, werden ungefragt verworfen.
  2. Die verschiedenen Leerräume haben eine unterschiedliche Wertigkeit. Wenn z.B. ein geschütztes Leerzeichen im Suchtreffer enthalten ist, sollte dieses bei der Ersetzung nicht durch ein einfaches Leerzeichen ausgetauscht werden.

Der erste Punkt ist unstrittig, hier muss um einen InDesign-Bug herumprogrammiert werden. Der zweite Punkt wird vermutlich je nach Ansicht des Anwenders bzw. Produkts unterschiedlich beantwortet werden. Deswegen muss man eventuell die Priorität der Zeichen, die erhalten bleiben sollen, individuell anpassen.

Wenn mit der Zeichenklasse \s arbeitet muss man sich zusätzlich noch überlegen, was bei der Zusammenführung mit den Formatierungseinstellungen der Absätze geschehen soll.
InDesign überträgt bei der Ersetzung die Einstellungen des vorletzten Absatzes auf den letzten Absatz. Ich bevorzuge aber das gegensätzliche Verhalten: Die Formateinstellungen des letzten Absatzes sollen erhalten bleiben. Mir reicht es zunächst, wenn das Absatzformat gerettet wird. Einzelne Einstellungen können aber nach dem gleichen Muster übernommen werden.

Um diese Probleme in den Griff zu kriegen habe ich ein Skript entwickelt. Es basiert auf dem findAndDo-Skript. Das gesamte Skript whitespacer.jsx steht zum Download bereit. Die interessantesten Teile stelle ich im folgenden vor:

Mit GREP werden alle Textstellen mit mindestens zwei aufeinanderfolgenden Leerzeichen gesucht:

app.findGrepPreferences.findWhat = "\\s{2,}";

Das Dokument muss rückwärts durchsucht werden, da Textänderungen den Index verschieben.

var _results = app.activeDocument.findGrep (true);   

Innerhalb einer for-Schleife werden die Suchergebnisse analysiert. Dazu wird mit indexOf geprüft, ob das Zeichen im Ergebnis vorhanden ist. Je weiter unten das Zeichen in Liste steht, desto höher ist die Priorität :

 // Normal Space
 if (_results[i].contents.indexOf ("\u0020") > -1) 
 _mostImportant = _results[i].contents.indexOf ("\u0020"); 
 // Nonbreaking Space
 if (_results[i].contents.indexOf ("\u00A0") > -1) 
 _mostImportant = _results[i].contents.indexOf ("\u00A0");
 // ..

Falls mehrere Absätze konsolidiert werden, merke ich mir das Absatzformat des letzten Absatzes. Hier müssen wiederum zwei Fälle unterschieden werden: Wenn der letzte Absatz mit Leerraum beginnt, gehört er zum Suchergebnis. Wenn nicht muss mit nextItem der nächste Absatz adressiert werden.

 var  _savedPStyle  = false;
 var _REbreak = RegExp("\\r","g");
 var _REStartsWhite = RegExp("^\\s");
 var _res = _results[i].contents.match (_REbreak);
 if (_res != null && _res.length > 1) {
   _lastPar = _results[i].paragraphs[-1];        
   if (_REwhite.test (_lastPar.contents) )
   _savedPStyle =_lastPar.appliedParagraphStyle;
   else 
   _savedPStyle =_results[i].paragraphs.nextItem (_lastPar ).appliedParagraphStyle; 
 }

Bei der eigentlichen Ersetzung werden alle Zeichen entfernt. Das zu erhaltende Zeichen, Marken, und bei der Suche ignorierte Zeichen werden geschützt:

 var _resArr = _results[i].characters.everyItem().getElements();
 for (var k = _resArr.length -1; k >= 0; k--) {
   var _char  = _resArr[k];
   if ( _char.contents != "\uFEFF" &&
        _char.contents  != SpecialCharacters.END_NESTED_STYLE &&
        _char.contents  != SpecialCharacters.INDENT_HERE_TAB &&
        _char.contents  != SpecialCharacters.DISCRETIONARY_HYPHEN &&
        _char.contents  != SpecialCharacters.DISCRETIONARY_LINE_BREAK &&
        _char.contents  != SpecialCharacters.ZERO_WIDTH_NONJOINER &&
        _char.contents  != SpecialCharacters.ZERO_WIDTH_JOINER &&
        k != _mostImportant)  _char.remove ();
   }
 }

Zu guter letzt wird das ehemalige Absatzformat erneut zugewiesen

 if (_savedPStyle) 
 _char.parentStory.characters[(_char.paragraphs[0].characters[-1].index + 1)].
paragraphs[0].appliedParagraphStyle;       
}

Das Skript ist noch nicht der Weisheit letzter Schluss. Insbesondere über die Performance müsste man noch nachdenken. Soweit ich das testen konnte läuft es sehr stabil.