USING TEXT FILES FOR DATA

While on the subject of working with plain text files (see recent blog posts), we will look at a script provided with InDesign. This script, FindChangeByList, uses a plain text file to take the place of what would otherwise require an extensive custom dialog. Although one of the most used of the scripts provided for InDesign, it is probably the least understood. In working through some of its processes it is hoped the reader will gain an appreciation for using text files for data. To start, the script reads a text file written by the user specifically to provide values for a number of find/change operations.

THE TEXT FILE

The text file works like a data table of sorts with each row being a data entry. The information for the elements of each entry are separated by a tab. This is what is referred to as a tab-return delimited text file.

  • The first element of an entry defines the type of search. This can be either “text”, “grep”, or “glyph”.
  • The second element is the text or grep expression to find.
  • The third element is the text to change to.
  • The fourth element determines the find/change options.
  • The last element is a comment.

If you open the FindChangeList.txt file (found in the FindChangeSupport folder of the Scripts panel) you will see a number of examples such as:

text {find what:"--"} {change to:"^_"} {include footnotes:true, include master pages:true, include hidden layers:true, whole word:false} Find all dash-dash and replace with an em dash.

The data in curly braces above could be replaced by the values for three variables in the following AppleScript: findPrefs, changePrefs, and findChangeOpts.

FindReplaceText

set errStr to "Expects text frame with target text to be selected"
--find all occurrences of two hyphens and replace with an em dash
try
   tell application "Adobe InDesign CC 2019"
       set findPrefs to {find what:"--"}
       set changePrefs to {change to: "^_"}
       set findChangeOpts to {include footnotes:true, ¬
include master pages:true, include hidden layers:true, whole word:false}
       if (count of documents) > 0 and selection is not {} then
	 set selList to selection
	 if class of item 1 of selList is text frame then
	     set myTextObject to object reference of text 1 of item 1 of selList
	     set myObject to parent of myTextObject
	     set numChanges to my myChangeText(myObject, findPrefs, changePrefs, findChangeOpts)
	     activate
	     display alert ("Number of items changed: " & numChanges)
	 else
   	     errer errStr
	 end if
       else
	 error errStr
       end if
   end tell
on error errStr
   activate
   display alert "Error: " & errStr
end try
(*Do text find/change using values passed*)
on myChangeText(myObject, findPrefs, changePrefs, findChangeOpts)
   tell application "Adobe InDesign CC 2019"
	set find text preferences to nothing
	set change text preferences to nothing
	set properties of find text preferences to findPrefs
	set properties of change text preferences to changePrefs
	set properties of find change text options to findChangeOpts
	tell myObject
		set myFoundItems to change text
	end tell
	set find text preferences to nothing
	set change text preferences to nothing
	set numChanges to length of myFoundItems
   end tell
   return numChanges
end myChangeText

The problem with this script is that it allows only one text find/change operation with its values hard-coded in the script. Tbis is of little value to the user. To make the script usable a custom dialog to define the find/change values needs to br added. Even with this it is of limited value as it works for a single find/change operation. To create a script with a custom dialog for more than one find/change operation, would be a real head-scratcher. So let’s see how Adobe’s FindChangeByList can allow any number of text, grep, or glyph find/change operations. The genius of this script is in how it utilizes a plain text file to provide these values.

THE FINDCHANGEBYLIST SCRIPT

The major steps for the FindChangeByList script can be outlined as follows:

Step 1: Get object for find/change. Make sure there is a document with text or one in which items are selected in a document. If there is a document with text but no selection the value of the myObject variable is the document. In this event, skip step 2 below. Otherwise if item 1 of the selection is a valid item the value of the variable myObject will be either the contents of the text frame selected, the parent story of the insertion point selected, or the text selected.

Step 2: Present user with dialog to define scope of find change. In the dialog the user chooses whether the find/changes should affect the: (a) Entire document (b) Selected story (c) Actual Text Selected

Step 3: Find text file to read. Find text file by name or if not found, have the user choose the text file.

Step 4: Read text file into a list of lists. Using the {return} delimiter the data elements are read as a list. The text items of each data element are converted to a list using AppleScript’s text item delimiter. This results in a list of lists of text items for each find/change entry.

Step 5: Call appropriate find/change handler. For each item in the list of lists (Step 4) call a handler based on the first element of each list item (“text”, “grep” or “glyph)

Step 6: Proess text data item. The specified handler creates and runs a script using the myObject variable and listitems from data item passed (find type, find preferences, change preferences, and find/change options)

Much of theses steps are fairly straigtforward. However, we will recreate most of the steps in a simpler script to make the process easier to understand.

OUR SIMPLIFIED SCRIPT

Step 1: Get object for find/change. Our demonstration script will assume that the user has a text frame selected containing the target text. Start the script in Script Editor by copying in the following:

try
   set myObject to getMyObject()
on error errStr
   activate
   display alert "Error: " & errStr
end try
on getMyObject()
   set errStr to "Expects text frame with target text to be selected"
      tell application "Adobe InDesign CC 2019"
	if (count of documents) > 0 and selection is not {} then
	   set selList to selection
	   if class of item 1 of selList is text frame then
		set myTextObject to object reference of text 1 of item 1 of selList
		set myObject to parent of myTextObject
	   else 
		error errStr
	   end if
	else
	   error errStr
	end if
   end tell
   return myObject
end getMyObject

If there is a selection in an open document with a text frame selected, the value of the variable myTextObject will be an object reference to the text inside the frame. With this, the script gets the parent of myTextObject (myObject variable) which is needed for a find/change.

Step 2: define scope for find/change operations Present user with dialog to define scope of find change. To keep our script simple, the scope will be the contents of the text frame selected.

Step 3: Find text file to read Because our script is to be compiled and read inside Script Editor, the path to the text file will be hard-coded in the script. To test this concept, copy the following into a new Script Editor document. Compile and run the script. If the file designated is not found you will be asked to locate the FindChangeList.txt file. (You will find the file in the “Applications:Adobe InDesign CC 2019:Scripts:Scripts Panel:Samples:AppleScript:FindChangeSupport” folder.)

set partFilePath to "Scripts:Scripts Panel:Samples:AppleScript:FindChangeSupport:"
set myFile to myFindFile(partFilePath)
myFile
(*Attempts to find text file. If not found, has user choose the file*)
on myFindFile(partFilePath)
   tell application "Adobe InDesign CC 2019"
	set appPath to file path as string
	set myFilePath to appPath & partFilePath & "FindChangeList.txt"
   end tell
   try
	set myAlias to myFilePath as alias
   on error
	set dLocation to (appPath & partFilePath) as alias
	set myAlias to choose file with prompt "Locate file for find/change" default location dLocation
   end try
   return myAlias
end myFindFile

The result of running this script will be an alias reference to the FindChangeList.txt file. If the text file is not found, the choose file method is used. Notice that our myFindFile handler adds a default location to the method. 

Step 4: Read text file into a list of lists. Once the FindChangeList.txt file is located, the script reads the file to create a list of lists for each data item in the file. Reading the file using the return delimiter creates a list of each data item. Inside each item, the data item’s text items are converted to a list using AppleScript’s text item delimiter defined as a tab. At this point we can assemble the pieces of our script for testing.

set partFilePath to "Scripts:Scripts Panel:Samples:AppleScript:FindChangeSupport:"
try
   set myObject to getMyObject()
   set myFile to myFindFile(partFilePath)
   set processList to myReadFile(myFile)
on error errStr
   activate
   display alert "Error: " & errStr
end try
(*Reads file in as a list of lists*)
on myReadFile(myFile)
   set processList to {}
   set fileRef to open for access (myFile)
   set myEOF to get eof fileRef
   set oldDelim to AppleScript's text item delimiters
   if myEOF > 0 then
      set AppleScript's text item delimiters to {"	"} --tab inside quotes
      set textList to read fileRef using delimiter {return}
      close access fileRef
      repeat with i from 1 to length of textList
         set myData to item i of textList
         if myData starts with "text" or myData starts with "grep" ¬
or myData starts with "glyph" then
             set end of processList to text items of myData
         end if
       end repeat
       set AppleScript's text item delimiters to oldDelim
    else
       error "Text file is empty"
    end if
    return processList
end myReadFile
--add getMyObject handler from above here
--Add myFindFile handler from above here.

Change the script to read as above and run. Script Editor’s Result window will show the result of reading the file. Notice that each data item starts with “grep” or “text” and is followed by a property for find what, change to, and a property record for find/change options.

Step 5: Call appropriate handler. Here the script repeats through the process list (processList variable) to call the processTextItem handler when a text item is encountered.

In the top portion of the script after the variable processList is defined, add the following:

repeat with i from 1 to length of processList
   set thisType to item 1 of item i of processList
   if thisType is "text" then
       processTextItem (myObject, item i of processList)
   end if
end repeat

Then start the processTextItem handler using the following:

on processTextItem(myObject, dataItem)
   activate
   display alert "processTextItem called"
end processTextItem

If you run the script at this point you should get two alerts indicating that the handler processTextItem was called.

Step 6: Processing the text data item.This is where the fun begins. What the script needs to do is take the data’s text items and convert them from text into script items. This can be done by concatenating the data’s text items into a scirpt that when run will set the find/change variables. Introducing do script.

DO SCRIPT

For do script, scripts are written as text. InDesign’s do script method then allows the text to be read and run as a script. To illustrate, create a new script in Script Editor and copy in the following:

set theScript to "set theText to \"Hello World\"" & return
set theScript to theScript & "activate" & return
set theScript to theScript & "display alert (theText)"

Compile and run the script. The Result window should show text written like a script with the exception that the entire text is surrounded by quotation marks and the quotes before the words “Hello World” are preceded with backslashes.

Now add the following at the bottom of this test script.

tell application "Adobe InDesign CC 2019"
   do script theScript language applescript language
end tell

When you run the test script an alert displays with the words “Hello World”.

Creating the do script to process the data items’ text elements for our processTextItem handler will be almost that easy. Just remember that string values need to be surrounded by quotation marks that are escaped using a backslash.

To test we will use the last item from the processList above for our data. Copy the following and paste into a new test script.

set dataItem to {"text", "{find what:\"--\"}", "{change to:\"^_\"}",¬
"{include footnotes:true, include master pages:true, include hidden layers:true, whole word:false}"}
copy dataItem to {myType, findPref, changePref, findChangeOpts}
set myScript to "tell application \"Adobe InDesign CC 2019\"" & return
set myScript to myScript & "set properties of find text preferences to " & findPref & return
set myScript to myScript & "set properties of change text preferences to " & changePref & return
set myScript to myScript & "set properties of find change text options to " & findChangeOpts & return
set myScript to myScript & "end tell"
myScript

Compile and run this test and view the result. The result should look similar to a script with the exception that it is enclosed in quotation marks and all the quotes aroound strings are escaped with back slashes. So far so good.

Now lets finish our processTextItem handler by adding the do script portion to code that does not require text to script conversion.

(*Uses do script to convert text to script items for a text find/change*)
on processTextItem(myObject, dataItem)
   copy dataItem to {myType, findPref, changePref, findChangeOpts}
   set myScript to "tell application \"Adobe InDesign CC 2019\"" & return
   set myScript to myScript & "set properties of find text preferences to " & findPref & return
   set myScript to myScript & "set properties of change text preferences to " & changePref & return
   set myScript to myScript & "set properties of find change text options to " & findChangeOpts & return
   set myScript to myScript & "end tell" 
   tell application "Adobe InDesign CC 2019"
      set find text preferences to nothing
      set change text preferences to nothing
      do script myScript language applescript language
      tell myObject 
         set foundItems to change text
      end tell
      set find text preferences to nothing
      set change text preferences to nothing
   end tell
end processTextItem

When you run the script with a text frame selected, occurrences of two hyphens in its text will be changed to an emdash and a hyphen surrounded by spaces will be changed to an endash.

A nice touch to the script would be to notify the user to the number of text items that were found and changed. Change the top portion of the script to read as follows.

FindChange_byList

set partFilePath to "Scripts:Scripts Panel:Samples:AppleScript:FindChangeSupport:"
try
   set myObject to getMyObject()
   set myFile to myFindFile(partFilePath)
   set processList to myReadFile(myFile)
   set textCount to 0
   repeat with i from 1 to length of processList
      set thisType to item 1 of item i of processList
      if thisType is "text" then
         set myCount to processTextItem(myObject, item i of processList)
         set textCount to textCount + myCount
      end if
   end repeat
   if textCount > 0 then
      activate
      display alert ("Text items processed: " & textCount)
   end if
on error errStr
   activate
   display alert "Error " & errStr
end try 

Add the following at the bottom of the processTextItem handler:

return length of foundItems

Revert your test document and run the script as modified above. You should get a dialog indicating the number of total text items found and changed.

ONWARD AND UPWARD

Our demonstration script is just the beginning. No, we didn’t write a script that could replace the famous FindChangeByList script but we did come up with some reusable handlers. And, hopefully you got a taste of what can be accomplished with do script when a text file is used to provide data. Coming next week, working with script code written as text.

Disclaimer:
Scripts provided are for demonstration and educational purposes. No representation is made as to their accuracy or completeness. Readers are advised to use the code at their own risk.