Writing Plugins Compatible with Versions 5, 6 & 7

Introduction

The change from Lua 5.1 to 5.3 in ƒh7, from Gedcom 5.5 to Gedcom 5.5.1, and the updating of many of the Lua libraries available for writing plugins poses some challenges to plugin authors who wish to write plugins for use across ƒh versions 5, 6 and 7. This article explores those challenges and offers advice on overcoming them.

General Guidance on Software Versions

It might be obvious, but just in case, check that any facilities you’re using in Lua, Library Modules or the ƒh API are available in all the versions of ƒh that you intend to support.

To test for the version of ƒh in use, for example:

if fhGetAppVersion () > 5 then
  --do something here
end

This should usually be sufficient to determine what version of Lua and Lua libraries are available to you, but you can also check the Lua version using:

if _VERSION == "Lua 5.3" then --FH 7
  --do something
else -- FH 5 or 6
 --do something else
end

Some but not all libraries will allow you to check their version, for example:

luasql = require "luasql"
ver = luasql._VERSION
print (ver)

If necessary, these checks will let you include coding for different versions of ƒh, Lua, and the libraries, should you (for example) want to make basic functionality available to all users but enhance the functionality where you can.

Lua 5.1/5.3 Compatibility

Some functions in Lua 5.1 have been renamed or deprecated in 5.3. This code block is needed near the beginning of any plugin written originally in Lua 5.1 to enable it to run in Lua 5.3 with the minimum of changes.

if _VERSION == "Lua 5.3" then
  -- handle changes to table library
  unpack = table.unpack --function moved in 5.3
  function table.getn(t) --function removed in 5.3
    local count = 0
    for _, __ in ipairs(t) do
      count = count + 1
    end
    return count
  end
  function table.maxn(t) --function removed in 5.3
    local max = 0
    for j,k in pairs (t) do
      if type(j) == "number" then
        max = j
      end
    end
    return max
  end
  --handle changes to string library
  loadstring   = load -- function removed in 5.3
  string.gfind = string.gmatch --function renamed in 5.3
end

local temp = os.tmpname()..".html" -- Includes path in Lua 5.3
if _VERSION == "Lua 5.1"
or fhGetAppVersion() <= 6 then
  temp = os.getenv("TEMP")..temp -- Prefix path in Lua 5.1
end

There are also changes that need to be made wherever particular Lua features have been used. The most common include:

--[[Byte zero pattern %z was deprecated in Lua 5.2 and removed in Lua 5.3 to be replaced by \0
The workaround is to specify byte0 and use it with this code in patterns instead of %z or \0 .
Patterns will need to be edited manually to include byte0 instead of %z or \0
--]]
local byte0 = "%z" --Byte zero pattern %z was deprecated in Lua 5.2 and removed in Lua 5.3 to be replaced by \0
if _VERSION == "Lua 5.3" then			 
  byte0 = "\0"
end

--[[in Family Historian 5 and 6 both % and the slash character were permitted as escape characters. Now only % is allowed]]--

--[[handle changes to OS.TMPNAME
--In 5.3 the os.temp variable now returns the full file name with thepath.  The following will make the call backwardly compatible with 5.1
--]]
local temp = os.tmpname()..'.html'
if _VERSION ~= 'Lua 5.1' then
  temp = os.getenv('TEMP')..filename
end

--[[Pseudo-argument arg
Pseudo-argument arg was deprecated in Lua 5.1 and removed in Lua 5.2.
Insert the following code into functions which use the ... argument
arg = {...}
arg['n'] = #arg
--]]

-- Example 
local function test(...)
  local arg = {...}
  arg['n'] = #arg
  for i,v in ipairs(arg) do
    print(i,v)
  end
end

--[[String formatting for numbers

string.format("%d",number) won't work if number is not an integer. It needs string.format("%d",math.floor(number)) or similar to ensure the parameter is an integer. That solution works for all Lua versions.

]]--

There are also changes in areas that are less commonly used. See the relevant Lua documentation to understand the cumulative changes from 5.1 to 5.3:

Library Changes

IUP

IUP is the most commonly used library, and the following code block should be including before IUP is used to make a plugin compatible with the changing versions of IUP.

require ("iuplua") -- this was optional before Family Historian 7
if tonumber(iup._VERSION) >= 3.28 then
  iup.SetGlobal("CUSTOMQUITMESSAGE","YES") --if you don't include this, FH will exit when the plugin exits
end

A popular simple dialogue involves iup.GetParam(..) but a bug prevents the button labels being changed from OK and Cancel. The workaround is:

function paramAction(iupDialog,intIndex)
  if intIndex == iup.GETPARAM_MAP then		-- Correct button labels needed for IUP 3.28 bug
    iupDialog.Button1.Title = "Edit"	-- Otherwise remain as OK and Cancel
    iupDialog.Button2.Title = "Exit"
  end
  return 1
end
local isAns,strAns = iup.GetParam("Test",paramAction,"%u[Edit,Exit]")	-- Displays dialogue with buttons Edit and Exit

function paramAction(iupDialog,intIndex)
  if intIndex == iup.GETPARAM_MAP then		-- Correct button labels needed for IUP 3.28 bug
    iupDialog.Button1.Title = "Apply Rules"
    iupDialog.Button2.Title = "Cancel Plugin"
  elseif intIndex == (iup.GETPARAM_HELP or -4) then	-- FH V5 needs -4
    fhShellExecute("https://pluginstore.family-historian.co.uk/page/help/main-help-page","","","open")
  end
  return 1
end -- function paramAction

Again, there are more changes than can be addressed here, and different plugin authors will have used different facilities. Consult the IUP History to understand what has changed and when.

Require and Loadrequire

In ƒh5 and 6, some library modules had to be downloaded before they could re required for the first time, via the Module Require with Load function; in ƒh7 these libraries are installed with [FH] and can simple be required.  See Lua References and Library Modules for details of the modules affected.  If writing for versions 5, 6, and 7 you should include the loadrequire snippet and code like:

if fhGetAppVersion () >= 7 then 
  socket = require ("socket")
else
  socket = loadrequire("socket")
end

User Written Modules

In Lua 5.3, user-written modules need to be created using tables (this was optional in Lua 5.1). For example,

function myModule()
  local x = 0 --private module constant
  local function myFunc1 (a,b)
    x = a+b
  end
  local function myGetX()
    return x
  end
  return{
    myFunc1 = myFunc1,
    myGetX = myGetX
  }
end

local M = myModule()
M.myFunc1(1,2)
print (M.myGetX()) -- '3'

Note: it is a requirement of the Plugin that each plugin can be run independently, without dependencies, so any modules must be included inline within the plugin.

Gedcom Data Format Changes

The change from Gedcom 5.5 to 5.1.1 brings with it some data format changes, including:

  • The structure for Media objects
  • Potential rich text in [FH7] note fields
  • Changes to the Gedcom tags for EMAIL (was _EMAIL), and WWW (was _WEB)

There’s a useful comparison of the two formats at GEDCOM Grammars : 5.5 vs 5.5.1

Whether these changes affect a plugin is highly dependent on what the plugin does, but if your plugin is affected you will need to write code for ‘old’ (versions 5 &6) and ‘new’ (version 7) and test the version of FH to determine which one applies.

 

Last update: 15 Dec 2020