Writing Internal Scripts using GENerel

The three tasks required before writing Internal Scripts. First, you need to know how to use GENerel, which is GEDitCOM II's built-in scripting language. Second, GEDitCOM II allows scripts to access all your genealogy data, but some access is done through the GEDCOM format. Thus, for some tasks, you need to be familiar with GEDCOM data structures. GEDCOM documentation can be found in the GEDitCOM II help or on line. Third, Internal Scripts work with commands and objects and you will often need to refer to available scripting commands and the dictionary of internal scripting objects.

Internal Scripts are preferred over AppleScripts in Python scripts for at least four reasons:

  1. Internal Scripts run 10 to 20 times faster. The reason is that Internal Scripts avoid all the overhead of inter-application communication with another run time environment.
  2. Even if script execution speed is not an issue (i.e. for a simple task), Internal Scripts are still much faster because then start immediately. AppleScript and Python scripts always have a short delay before starting while loading a run time environment.
  3. Internal Scripts will continue to work even if Apple decides to delete things like access to AppleScript or Python. Apple has done things like that in the past.
  4. Internal Scripts use the same language used to customize the user interface.

Internal Scripting was introduced in version 3.0 of GEDitCOM II. Many prior extensions have been rewritten (now known as "John's" Version) as well is many prior scripts embedded in browser styles. Whenever possible, more will be converted and future extensions will use Internal Scripting.

The help on this page covers the following topics for writing Internal Scripts:

Hello GEDitCOM II

When learning a new programming tool, the first step is often a trivial program to see something happen. Here is a trivial Internal Script for GEDitCOM II:

! look for open document
@gdoc = "gcapp.frontDocument"
ifndef gdoc
  exit "No documents are open."
endif

! look for selected records
@recs = "gdoc.selectedRecords"
if @recs.count=0
  exit "No record is selected."
endif

! get first one and write it's name
write "The selected record's name is "&@recs.0.name

To try this script do the following:

When starting Internal Scripts, the object variable gcapp is automically defined and corresponds to the application class (see the next section to learn about variables and objects in Internal Scripts). The script uses the application object's "frontDocument" attribute to look up the front document and sets gdoc to that document object. If no document is opened, the script exits with a message to the scriting panel. If a document is open, the script fetches a list of selected records and stores the list in recs. If the list is empty, the script exits with a message to the scripting panel. If records are selected, the script writes the name of the first selected record and then ends. The name is found with an "@" expression (@recs.0.nameM). Note that "@" expressions in Internal Scripts change from getting GEDCOM data for interface customization to a method to reading properties of objects.

Note that assigning objects as in the first command above is a convenience method available since 3.5/1.0. The method used in earlier versions (and still supported) was the get command. Two commands with identical results are:

@gdoc = "gcapp.frontDocument"
gcapp.get gdoc,"frontDocument"

This documentation will usually use the assignment method because it is easier to understand and more similar to other languages (although Python sometimes needs get() to assign objects).

To start developing this (or any other script), go back to the GEDitCOM Editor and add Internal Scripting code. Although the above steps work for writing scripts, it is much better to package scripts into a GEDitCOM II "Plug In" or "Extension". Finally, when developing Internal Scripts, it can be helpful to see example scripts. Open any of the GEDitCOM II "Plug In" or "Extension" files in the GEDitCOM Editor and expand the "Scripts" section. Any file listed in that section with extension .gcscpt is an Internal Script.

GENerel Langage for Internal Scripts

An advantage of Internal Scripts is that that they use the same GENerel language that is used to customize the user interface, but these two tasks have different needs and therefore have these differences:

  1. Although scripts and interface customization use the same GENerel language, Internal Scripting adds scripting objects and a few additional commands. The objects are used to access genealogy data in GEDitCOM II and for many other tasks.
  2. The GENerel Commands that are specific for interface customizations are not relevant to Internal Scripts.
  3. While AppleScript and Python scripting frequently refers to the GEDitCOM II Suite in its AppleScripting dictionary to learn about classes and commands, Internal Scripts should refer instead to the Dictionary of Internal Scripting Objects and to available scripting commands.
  4. Internal Scripts support all useful scripting commands available to AppleScript and Python and the format for calling them in Internal Scripts is provided. Internal Scripting also defines many additional scripting commands.
  5. Although variables do not have to begin "#", some commands for Internal Scripts require it (which is done to guarantee no conflict with scripting objects). To make sure all such features work, Internal Scripts should use "#" at the start of all variable names (and note that object names cannot contain "#" at all).
  6. Internal Scripts have access to fewer read-only variables (because most of the read-only variables for interface customization are not relevant to Internal Scripts). The read-only variables for Internal Scripts are:

Working with Genealogy Documents

All Internal Scripts define gcapp as the object name for the GEDitCOM II Genealogy Application object (and the script should not change gcapp to something else). You can use gcapp to access currently opened documents using;

@docs = "gcapp.documents"
if @docs.count>0
  @doc = "docs.0"
  ! doc is opened front document (or pick another number)
endif                              
@doc = "gcapp.frontDocument"
ifDef doc
  ! doc is opened front document
endif                              

The first block gets an ordered GC List of opened documents. If the list has any members, the first one (at index 0) is the front document. The second block uses a "to-single object" attribute of a Genealogy Application object to get just the front document. When a "to-single-object" attribute of any object finds nothing, it returns a special None object. The ifDef conditional will be true if a document was found or false if a "None" object was returned.

Almost all scripts work on one or more documents and some might need scripting features that only became available in certain versions of GEDitCOM II (or GEDitCOM if writing scripts that work in both). The best way to start such scripts is:

PreChecks "my script name",3.5,1,1,1
@doc = "gcapp.frontDocument"

The first line verifies the user is running GEDitCOM II, version 3.5, build 1 (2nd and 3rd parameters) or GEDitCOM, version 1.0, build 1 (4th and 5th parameters) and that at least one document is open. For more details, see the PreChecks command. If all checks are OK, the script continues and it is guaranteed to find an open document in the second line.

You can create a new document using:

CreateDocument
@doc = "gcapp.frontDocument"

The second line sets doc to object name for the new document.

GEDitCOM II documents have many object relationships and properties as defined here.

Working with Genealogy Records

Accessing Records

You can read data from any GEDCOM record by getting the record's object and then using any attribute or property of GEDCOM records (or subclasses of GEDCOM records). You can get records from a list by number, name, or id as follows:

@records = "gdoc.gedcomRecords"
write @records.6.id&return
@rec = "records.@I34@"
write @rec.name&return

the first line gets a GC List of records in the document. The second line uses record number, which is the order of the records in the file (numbering starts with 0). The third line gets a GEDCOM record by its GEDCOM record id.

IF you are focusing on one class of records (such as just individual or family records), it is faster to query just those records:

@indis = "gdoc.individuals"
write @indis.5.name&return
@fams = "gdoc.families"
@fam = "fams.@F27@"
write @fam.marriageDate&return

See the internal scripting objects for details on attributes and properties of GEDCOM records and all the subclasses of GEDCOM records.

Creating Records

You create records by adding them to a document with the addRecord command:

doc.addRecord rec,"INDI","male"
@rec.name = "John Doe"
@rec.birthDate = "12/12/1954"

The type of record requested is added to the document and stored in the object name used in the command (here it is rec). Once the record is added, you can set any available properties (see the internal scripting objects for details on properties of GEDCOM records and all the subclasses of GEDCOM records).

You can create custom record types by using a custom type in the second argument to the command.

Moving/Dropping Records

The dropOn command moves any record to another record and it will mimic the drag and drop features of GEDitCOM II. In other words, the dropOn command is the same as dropping a record on the record browser of another record. Some examples are:

@indis = "gdoc.individuals"
@fams = "gdoc.families"
indis.@I7@.dropOn fams.@F27@

@sours = "gdoc.sources"
sours.2.dropOn indis.@I41@

@objes = "gdoc.multimedia"
#name1="Smith, John, Jr."
#name2="John Smith Portrait"
objes.#name2.dropOn indis.#name1

See the editing help in GEDitCOM II for each type of record to know which moves (analogous to drops) are supported. Some common moves are

Deleting Records

If objName is a GEDCOM Record (or any subclass of GEDCOM Record), you can delete it with:

objName.delete

The record will be deleted from its document and removed from any GC list that contains it. It will not however, be removed from any lists created by the script. To delete and remove from any list, you can use instead:

mylist.2.delete

This approach fetches the record from a list and because it knows the list, it will remove from that list as well.

Locating Records

The AppleScripting feature to locate "every" record with some properties with simple coding, is not implemented for Internal Scripting. The same task, however, can be done by looping over records, such as:

@allindis = "gdoc.individuals"
SDNRange "#b","FROM 1920 TO 1929"
CreateList indis
RepeatWith indi from allindis.count
  if @indi.birthSDN>=#b[1] && @indi.birthSDNMax<=#b[2]
    indis.addObject indi
  endif
EndRepeat

The list in indis will have all individuals born in the provided date range. This coding is more complex, but overall speed is still faster than an AppleScript.

Working with GEDCOM Data in Records

The following options can read or change data in GEDCOM records:

  1. Directly read or set properties for a record
  2. Read or edit any GEDCOM structure
  3. Read or edit raw GEDCOM data
  4. Honoring record restriction

Directly read or set properties for a record

The different types of GEDCOM records as well as GEDCOM structures have properties that can be "read" and "set." Properties are read using "@" expressions:

@doc = "gcapp.frontDocument"
! read birth date of one individual
@indis = "doc.individuals"
#bd = @indis.1.birthDate

Any property listed in the internal scripting objects can be read. Properties not specified as read only ("r/o") can also be changed with a setting expression or a set command. For example:

! change the name
@indis.3.name = "Henry James"

Although only some properties can be set using the above method, all other data can be edited using methods in the following sections.

Read or edit any GEDCOM structures in a record or in another structure

GEDCOM records are organized into GEDCOM structures . You can read any structure that is not hard-wired to a property by using the evaluateExpression command such as

indi.6.evaluateExpression "#cd","CENS.DATE"

This command will read the date for the first census event in the individual 6 by using a GEDCOM expression (without the "@" sign) and set the result to variable #cd. For more details on this command, see the evaluateExpression command.

You can directly access GEDCOM structures by number or name (where name refers to the GEDCOM tag word for the structure) and then work with the properties of that structure. For example, to access and change the NCHI structure (or number of children, if it exists) of a family record, you can:

@structs = "fams.@F23@.structures"
@structs.NCHI.contents = 3

The second line means you can set the contents of any structure, where "contents" is the line value for the first line of the structure. You can also change the GEDCOM tag (using the name property), but you cannot change the level number. If there is more than one structure with the same name, accessing it by name method will return the first one. The best way to access multiple structures, or to access structures without using name or number, is to use the findStructures command.

GEDCOM structures themselves are organized into subordinate structures. You can access any substructure by number or name within the parent's structures. For example, the divorce date of an family might be set using

@structs = "fams.23.structures"
@dsub = "structs.DIV.strutures"
@dsub.DATE.contents = "12 JUL 1845"

The above method assumes the structure you want to access already exist. If they do not exist, you may want to create and add them.

You can add new structures to GEDCOM records or subordinate to other GEDCOM structures using the addStructure command:

indi.addStructure struct,"BIRT","",2
struct.addStructure bd,"DATE","4 Jul 1776"

The first addStructure adds a BIRT line to an individual with object name indi. The second one adds a DATE line subordinate to the BIRT line.

You can delete structures using the delete command:

@structs = "indi.structures"
structs.2.delete

Edit or edit raw GEDCOM data

You can access raw GEDCOM data using the gedcom or gedcomArray property of GEDCOM records and GEDCOM structures. The first gives a string with GEDCOM data while the second gives a list with each element in the list having details about each line.

You can read data for any GEDCOM object, but you can only set gedcom property for GEDCOM records. Because setting GEDCOM data could corrupt the data in your file, such as to break existing linkages between records, tt should only be used by those very familiar with the GEDCOM format and usually only for special applications. To change GEDCOM data for the individual with object name indi:

#recText = @indi.gedcom
   (... use any commands to change #recText)
@indi.gedcom = #recText

The first line of the new GEDCOM data must begin in "0" (to indicate it is a record), but the ID and record type in that first line will be ignored and replaced by values in the original record. In other words, this approach only changes subordinate data within the record.

Honoring record restriction

A user can restrict editing of records in GEDitCOM II using a GEDCOM RESN tag. Such a restriction can prevent editing of the record in GEDitCOM II, but does not block scripts in extensions from changing the data. A robust extension should either check for restrictions before changing any data, or document in extension help that it might change locked records.

Internal scripts can check for restrictions using:

if @rec.restriction!="locked"
  ! it is "unlocked", "privacy", or "confidential"
else
  ! it is locked so do not change it
endif

where rec is the record object. Note that only individual or family records can be locked.

Working with Dates

A common need in scripts is to work with dates in your file. The challenge in genealogy is that dates are often incomplete. For example you might know your great-grandfather was born in 1872 but not know the month or day. Similarly a date might be a month and a year (MAR 1842) or even a range of dates (BET 1900 and 1911). These "fuzzy" dates are confusing to many programming languages. The built-in date features of most programming languages expect all dates to be an exact date like "12 JUN 1949" and therefore do not know how to handle genealogy dates. Fortunately, GEDitCOM II scripting provides tools for working with "fuzzy" dates.

The basic task for all date calculations is to convert the date to a serial day number (SDN) which assigns an increasing number to all days with day 1 being a date around 4000 BC. When the date is a "fuzzy" date, the single SDN is represented by a range in day numbers spanning the earliest possible day represented by that date to the last possible day. For example the "fuzzy" date 1872 would correspond to the SDN range {2404794, 2405158} where 240474 is the SDN of 1 JAN 1872 and 2405158 is the SDN for 31 DEC 1872.

In date scripts, the first task is fetch the SDNs or SDN ranges for dates in your file. Many of the most common dates are available as properties of records. Some examples are:

#bdmin = @indi.birth
#bdmax = @indi.birthSDNMax
CreateList ddrange,@indi.deathSDN,@indi,deathSDNMax
CreateList mrang,@fam.marriageSDN,@marriageSDNMax
@structs = "indi.structures"
#cd = @struct.CENS.eventSDN

Here indi and fam are assumed to be valid object names for an individual or a family record. The SDN property gets the minimum SDN for the given date; the SDN max property gets the maximum SDN for the given date. If the date is not available, the returned SDN is zero.

For dates not connected to a property or for dates just in a text string, you can use the SDNRange function. For example, you can get today's SDN:

SDNRange "#var",#today$

which on 13 May 2022 returns #var[1] = #var[2] = 2459713 (note that the SDN range for an exact date has the same minimum and maximum).

Once you have SDN's for dates, calculations are simple. For example:

#ageAtDeath = (@indi.deathSDN - @indi.birthSDN)/365.25       ! age based on minimum SDNs
#minAge = (@indi.deathSDN - @indi.birthSDNMAX)/365.25        ! minimum possible age
#maxAge to (@indi.deathSDNmax - @indi.birthSDN)/365.25       ! maximum possible age

To convert back to a date (using the user-preferred date style), you can use the DateText command:

DateText "#var",2455032       ! #var =  "19 Jul 2009"

Lastly, for more advanced calculations, the Date Calculator application (which is provided with GEDitCOM II in the "GEDitCOM Extras" folder in your "Applications" folder) can do more date calculations.

Working with Albums

You can get a list of albums currently in the document or create a new album with

@albums = "gdoc.albums"
@album = "albums.0"

or

gdoc.addAlbum album,"Name of the Album"

Once you have an album, you can add or remove any GEDCOM Record to or from album using the addRecord and remove commands:

album.addRecord obj1,obj2,...
album.remove objName

where objName is a GEDCOM Record. You can also add records to albums or manipulate them by other methods such as the populate, union, intersection, and difference.

You can delete and entire album with album.delete. This style of delete will delete the album, but will not remove it from the list of albums described above. To have it removed from that list automatically, reference the album to delete from that list, such as with albums.2.delete.

Working with Lists

Many lists provided to Internal Scripts (such as a list of individuals) are immutable lists known as GC Lists. You can read from these lists, but cannot change them. For more list options, Internal Scripting can use Lists. You can create a List and add elements with

CreateList myList
myList.addObject obj1,obj2,...
myList.addString expr1,expr2,...
myList.addList otherList,...

Once created, you can manipulate elements in a list using any of the commands supported by List

Warning: Although Internal Scripts work fine with very large lists, creating a list in an Internal Script and then filling it by repeated calls to addObject or addString can get slow as the list grows. This issue appears to be caused by MacOS handling of mutable lists. The scripts will still work. If they get too slow, try to find a different approach to the same problem (future versions of GEDitCOM II will look for faster list methods than standard MacOS list objects).

Working with Dictionaries

Internal Scripts can make use of dictionaries to store information in key-value pairs. The key is always a string. The values can be objects, strings, or numbers. You can create a dictionary and add key-value pairs with the following commands:

CreateDictionary myDict
myDict.setObjectForKey obj,key
myDict.setStringForKey expr,key
myDict.setNumberForKey expr,key

Alternative short cuts for setStringForKey command are:

@myDict.key = expr
myDict.set expr,key

These short cuts, however, will set that key as a string value even if expr evaluates to a number. The "@" option can either be a dictionary or an object specifier that evaluates to a dictionary (note: these methods cannot change a key reserved by any dictionary property and are only available since 3.5/1.0).

Once created, you can access or remove key value pairs with these two commands:

myDict.valueForKey ("#var" or objName),key
myDict.removeValueForKey key

where "#var" should be used to access strings or numbers and objName should be used to access objects. If a key's value is a string or a number, you can it read using an "@" expression or include within any expression:

#value=@myDict.key
#explain="The value for key is "&@myDict.key

You can also access lists of all values, all keys, and keys sorted by various methods from the to-many attributes of the dictionary. A script can loop through these lists of keys to read values in the dictionary. For example, if a dictionary has a mix of strings, numbers, and objects, you can access them without needing to know their type as in the following example, which iterates through all keys:

@keys = "myDict.allKeys"
RepeatWith "#key" from keys
  myDict.valueForKey "#var",#key
  ifDef #var
    write "Key "&quote&#key&quote&" has a string or number"&return
  else
    myDict.valueForKey objVar,#key
    write "Key "&quote&#key&quote&" has an object"&return
  endif
EndRepeat

Writing Customizable Reports

GEDitCOM II has numerous built-in reports accessible through the reports menu. But you are not limited to those reports. By writing a custom script, you can define new reports and display them in a record browser the same way built-in reports are displayed.

Internal Scripts provide a special ScriptOutput object that simplifies the process of creating reports. It supports three report styles:

  1. HTML reports
  2. Plain text reports
  3. Monospace reports

HTML reports

The following is an outline of an Internal Script to create an html report:

@gdoc = "gcapp.frontDocument"
CreateScriptOutput rpt,"My Custom Report","html"

rpt.out "<p>This is a report about document "&@gdoc.name&"</p>"&return
! insert more html content using rpt.out commands

rpt.head (text)         ! add text to a <head> element
rpt.css (text)          ! and CSS items to the report

rpt.write gdoc,"<h1>Introduction</h1>"&return

The first line assigns object name gdoc to the front document. The second line creates the ScriptOutput object that manages the report. The bulk of the script adds html content to the report using the rpt.out command. You need to know enough about using html to work with such reports, but you can use any valid html commands. The rpt.head and rpt.css can add custom options for the html content. Note that rpt.out does not parse for special characters such as "\n" in a string; it will just include those characters. You can use "return" in string expressions (as done above) to insert new lines in the report.

When the report is the done the final rpt.write compiles all report elements and opens it in a report window. The optional second argument to rpt.write inserts any text at the start of the report. Sometimes a report needs to include information at the start of the report that is not known before the report is compiled. This "preface" argument option solves that issue for such reports. See help on the rpt.write for a option to export the report to a file.

One nice feature of html content is providing links to other web sites. An additional feature in GEDitCOM II is that your report can provide links to open any record in the file. Links to records can use two different styles:

<a href='geditcom:@I128@'>Smith, John</a>
<a href='//@I128@'>Smith, John</a>

These links are standard html "<a href=" link, but the reference must either start with "geditcom:", which stands for "GEDitCOM scheme," or start with two slashes "//". Although both work, the "geditcom:" scheme is recommended because it is the preferred method for custom URL links (note that version 3.5/1.0 is required for "geditcom:" scheme to work for any MacOS and for two slashes to work in MacOS 27; GEDitCOM II version 3.4 or older requires two slashes but that method stops working in MacOS 27 until upgrading 3.5/1.0 or newer). The rest of link is any record ID (which can be read with @rec.id where rec is a record's object name). The text inside the link is the clickable text that will appear in the report. Besides links to records or outside web sites, you can add internal links within the report by standard html methods. Such links are helpful in long reports.

By default, html reports use the default cascading style sheet (CSS) used for built-in GEDitCOM II reports. If needed, you can change them or create new cascading style sheet options using the rpt.css command. They can be added at any time and the rpt.write command handles inserting them in the proper place when the final report is created. Similarly, you can add html <head> elements, such as entities, by using the rpt.head command.

Some of the extensions provided with GEDitCOM II create custom reports using ScriptOutput. You can refer to documentation and scripts in those extensions for more examples of creating such reports. Although ScriptOutput objects have many properties, none of them affect html style reports.

Finally, documented MacOS methods for printing html content do not seem to work correctly. Html reports will always display well, but you may need some special metnods to print them (as documented in the GEDitCOM II help files).

Plain text reports

The following is an outline of an Internal Script to create a plain text report (you can try it out as explained above):

@gdoc = "gcapp.frontDocument"
CreateScriptOutput rpt,"My Custom Report","plain"

rpt.out "A line of text for the report."&return
! insert more text rpt.out commands

rpt.moveto 0,25
rpt.out "Text starting in column 25"

rpt.write gdoc

The first line assigns object name gdoc to the front document. The second line creates the ScriptOutput object that manages the report. The bulk of the script adds text to the report using the rpt.out command. To help aligning text (if using a monospaced font), you can use the rpt.moveto to move to a column on the the line. The next text added to the report will start at that column.

You can customize the report with ScriptOutput properties. The only properties used for plain text reports are monoFont, monoSize, printSize, and monoCol. Despite the property names, you do not need to use a monospaced font (unless you want to align text in columns).

When the report is done, the final rpt.write will output the results, which can be to a report window or exported to a file (see rpt.write for details).

Monospace reports

A monospace report is a plain text report with extra features for lining up text in columns and outputing pages with headers and footers. The following is an outline of an Internal Script to create an monospace report :

@gdoc = "gcapp.frontDocument"
CreateScriptOutput rpt,"My Custom Report","monospace"

@rpt.headerText = "My Report"
@rpt.footerText = "Page [#]"

rpt.moveto 5,25
rpt.out "Report Title"
rpt.newline 2

rpt.out "Name",30,"Birth Date",45,"Death Date",60
@recs = "gdoc.selectedRecordsINDI"
RepeatWith rec from recs
  rpt.out @rec.name,30
  rpt.out @rec.birthDateUser,45,@rec.deathDateUser
EndRepeat

rpt.write gdoc

The first line assigns object name gdoc to the front document. The second line creates the ScriptOutput object that manages the report. The rest of the commands define page style and add text content.

When the report is the done, the final rpt.write will output the results, which can be to a report window or exported to a file (see rpt.write for details).

Page Style

Monospaced reports uses a page buffering method and is designed to use a monospaced font. The font for the report is set using ScriptOutput properties monoFont and monoSize. The number of rows and columns for a page are set using rowsPerPage and colsPerRow properties.

Each page can have a one-line header and/or footer. These are configured using ScriptOutput properties headerText, headerAlign, footerText, and footerAlign. Either one can display a page number, which is set with the monoPage property. You can control margins and gap between header and footer and page text using topMargin, bottomMargin, and textGap properties.

To have page style apply to the first page, set all needed properties before adding any text to the report. The properties can also be changed after adding text to the report but any changes will not take effect until the next page begins.

Adding Text to the Report

Text is added to a report using the rpt.out (see that command for more details). The two options are to use the rpt.moveto command to move to a position on the page and then use rpt.out to insert some text, or to use a multi-argument rpt.out command to output text in fields defined by column numbers. The text can he left justified, centered, or right-justified in each field.

The report automatically starts a new line when current column reaches the report's colsPerRow setting or an rpt.out command requests a column before the current column position. You cannot insert return characters into the report (they will be changed to spaces if you do). If you need to move to the next line, use the newline command.

Text added to a monospace report is adding to a page buffer. You can move around that buffer with the rpt.moveto. When you insert text it will be at the current location even if that means the new text will overwrite previously inserted text. Page breaks will automatically occur when the current row exceeds rowsPerPage. To induce a page break at other times use the rpt.newline command with the number new lines greater than the rowsPerPage property.

You can read the current row and column position with ScriptOutput read-only monoRow and monoCol properties.

Printing Monospace Reports

Two reasons for using monospace reports instead of html reports is to easily align things without learning all html options and because the reports can print with better pagination (printing html reports normally requires extra steps and random page breaks). Getting printed reports with accurate page breaks and aligned columns requires three steps:

  1. Use a monospaced font (i.e., all characters the same width) such a "Courier" or "Monaco." Set using the monoFont property.
  2. Pick font size for printing using the printSize property. Font sizes that looks good on high-resolution monitors tend to be too large when printed. To solve this issue, printSize lets a report specify a different font size to be used only when the report is printed.
  3. Set the columnsPerRow and rowsPerPage properties to exactly match the space available on the printed page. These settings will depend on font, font size, paper size, and margins created with the "Page Setup..." menu command.

The last step above might need some trial and error reports to get exact values. The following table gives some results for rowsPerPage X columnsPerRow for two different fonts and either US Letter or A4 paper. These values assume printing using the default GEDitCOM II margins or 72 pixels on each edge of the page.

Font and SizeUS LetterA4 Font and SizeUS LetterA4
Courier 960 X 8865 X 84 Monaco 956 X 8861 X 84
Courier 1055 X 7959 X 76 Monaco 1048 X 7951 X 76
Courier 1151 X 7155 X 69 Monaco 1144 X 7148 X 69
Courier 1247 X 6551 X 63 Monaco 1241 X 6544 X 63

It is possible these values will need to changes in different MacOS software or with different printers. A ScriptOutput object defaults to "Courier,", display size 12, print size 9, and 60 X 88 rows and columns for US Letter paper

Text Output from a Script

All extensions running in GEDitCOM II open the scripting panel while any scripts in those extensions are running. By default, this scripting panel disappears when the script is done, but you can also use that panel to provide information to the user or even to provided the primary output of the script. Here are the rules for the scripting panel: