Create Source Template Definitions.fh_lua--[[
@Title: Create Source Template Definitions
@Type: Standard
@Author: Mark Draper
@Version: 1.2
@LastUpdated: 28 Apr 2025
@Licence: This plugin is copyright (c) 2025 Mark Draper and is licensed under the MIT License which
is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Creates a Template Definition for user-defined or imported Source Templates where this does
not exist already, and saves it to the relevant Family Historian Source Templates collection.
This enables the definitions to be used in other Projects.
]]
--[[
Version 1.2 (Apr 2025)
- Multi-monitor support added (dedicated form rather than simple iup alarmm)
]]
fhInitialise(7,0,0, 'save_recommended')
fhu = require('fhUtils')
fhu.setIupDefaults()
function main()
local pT = fhNewItemPtr()
pT:MoveToFirstRecord('_SRCT')
-- check if project is using templates
if pT:IsNull() then
MessageBox('This project does not use Templated Sources.', 'OK', 'ERROR')
return
end
-- check for duplicate names
if CheckDuplicates() then return end
-- read in available template definitions
local tblCollections, tblHeaders = Initialize()
-- check Project templates for missing definitions and links
local tblMissing = {}
local tblX = {}
while pT:IsNotNull() do
if MissingTemplate(pT, tblCollections) then
table.insert(tblMissing, pT:Clone())
end
if fhCallBuiltInFunction('LinksTo', pT) == 0 then table.insert(tblX, pT:Clone()) end
pT:MoveNext()
end
if #tblMissing == 0 then
local Msg = 'All validated Source Templates have a corresponding definition.\n\n' ..
'Select "Tools>Source Template Definitions from the main menu to check that ' ..
'they are fully synchronized.'
MessageBox(Msg, 'OK', 'INFORMATION')
return
end
local Msg = '\nThere are ' .. #tblMissing .. ' Source Templates in this Project without a ' ..
'corresponding Source Template Definition.\n\n'
if #tblMissing == 1 then Msg = '\nThere is 1 Source Template in this Project without a ' ..
'corresponding Source Template Definition.\n\n' end
repeat
local Option = AlarmForm('Create Source Template Definitions (1.2)', Msg,
'Create Definitions', 'Help', 'Close')
if Option == 2 then
local H = 'https://pluginstore.family-historian.co.uk/help/create-source-template-definitions'
fhShellExecute(H) -- keep line length below self-imposed style limit
elseif Option == 0 or Option == 3 then return end
until Option == 1
-- ask for permission to make irreversible changes
Msg = 'Updating Source Template Definitions cannot be reversed by using Edit > Undo Plugin Updates. ' ..
'Undoing the changes will require restoring the relevant template definition file(s) from a ' ..
'back up (see the plugin help for more details on where definition files are stored).\n\n' ..
'Are you sure that you want to proceed?'
if MessageBox(Msg, 'OKCANCEL', 'WARNING', nil, 2) ~= 1 then return end
-- work through missing template definitions
local tblUpdated = {} -- new or updated collections
for _, Template in ipairs(tblMissing) do
GenerateTemplate(Template, tblCollections)
tblUpdated[fhGetItemText(Template, '~.COLL')] = true
end
for Collection, _ in pairs(tblUpdated) do
UpdateDefinitionFile(Collection, tblCollections, tblHeaders)
end
local Msg = 'Template update complete.\n\nSelect "Tools>Source Template Definitions..." from the ' ..
'main menu to check that they are fully synchronized.'
if #tblX > 0 then Msg = Msg .. '\n\n' .. #tblX .. ' Source Templates have no linked sources. ' ..
'Do you want to delete these from your project?'
if MessageBox(Msg, 'YESNO', 'QUESTION', nil, 2) == 1 then
for _, Template in ipairs(tblX) do fhDeleteItem(Template) end
fhUpdateDisplay()
end
else
MessageBox(Msg, 'OK', 'INFORMATION')
end
end
-- ******************************************************************************************************--
function AlarmForm(formtitle, heading, txt1, txt2, txt3)
-- create form modelled on iup.alarm (full form supports parent in multi-monitor setup)
local action = 0
local btn1 = iup.button{title = txt1, padding = '10x3',
action = function(self) action = 1 return iup.CLOSE end}
local btn2 = iup.button{title = txt2, padding = '10x3',
action = function(self) action = 2 return iup.CLOSE end}
local btn3 = iup.button{title = txt3, padding = '10x3',
action = function(self) action = 3 return iup.CLOSE end}
local buttons = iup.hbox{iup.fill{}, btn1, btn2, btn3, iup.fill{};
margin = '0x10', normalizesize = 'Both', padding = 10, gap = 20}
local label = iup.label{title = heading}
local vbox = iup.vbox{label, buttons; margin = '20x20'}
local dialog = iup.dialog{vbox; title = formtitle, resize = 'No', minbox = 'No'}
if fhGetAppVersion() > 6 then
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
end
dialog:popup()
return action
end
-- ******************************************************************************************************--
function CheckDuplicates()
-- checks for duplicated Template name
local tblT = {}
local pT = fhNewItemPtr()
pT:MoveToFirstRecord('_SRCT')
while pT:IsNotNull() do
local Name = fhGetItemText(pT, '~.COLL') .. fhGetItemText(pT, '~.NAME')
if tblT[Name] then
local Msg = 'There are multiple Templates called ' .. fhGetItemText(pT, '~.NAME') .. ' in ' ..
'the ' .. fhGetItemText(pT, '~.COLL') .. ' Collection. Please ensure that Templates ' ..
'have a unique name within their Collection prior to creating the Template Definitions.'
MessageBox(Msg, 'OK', 'ERROR')
return true
end
tblT[Name] = true
pT:MoveNext()
end
end
-- ******************************************************************************************************--
function Initialize()
--[[
Builds up a table of Template Collections.
tblD is a table of Template definitions (Collection name => table of Templates in that Collection).
tblH is a table of header information (Collection name => table of XML lines prior to
the Template definitions in the Collection file).
This method enforces unique Template names within a given Collection, although FH permits duplicate names.
]]
local field
local Path = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Source Templates\\Custom'
local TemplateName
local tblD = {}
local tblH = {}
local FSO = luacom.CreateObject('Scripting.FileSystemObject')
local objFolder = FSO:GetFolder(Path)
for _, File in luacom.pairs(objFolder.Files) do
if File.Name:match('.fhst$') then
local Collection = File.Name:sub(1, -6) -- remove extension
local FileContents = fhLoadTextFile(File.Path)
if FileContents then
local tblC = {}
local tblT = {}
local header = true
for line in FileContents:gmatch('[^\r\n]+') do
if line == '' then
if header then tblH[Collection] = tblT end -- end of file header
tblT = {}
header = false
field = false
elseif line == '' then
field = true
end
table.insert(tblT, line)
local _, T, _ = line:match('^()(.+)( )$')
if T and not field then
TemplateName = T:gsub('&', '&'):gsub('<', '<'):gsub('>', '>')
end
if line == ' ' then
tblC[TemplateName] = tblT -- save completed template
tblT = {} -- clear table
end
end
tblD[Collection] = tblC -- save collection
end
end
end
return tblD, tblH
end
-- ******************************************************************************************************--
function MissingTemplate(pT, tblCollections)
local Collection = fhGetItemText(pT, '~.COLL')
local Name = fhGetItemText(pT, '~.NAME')
if Name == '' then
MessageBox('Unnamed Template definitions cannot be exported', 'OK', 'WARNING')
return
elseif Collection == '' then
MessageBox('Source Template Record ' .. Name .. ' does not have a defined ' ..
'Collection, so cannot be exported.', 'OK', 'WARNING')
return
elseif Collection == 'Essentials' or Collection == 'Advanced' then
return
elseif tblCollections[Collection] and tblCollections[Collection][Name] then
return
else
return true
end
end
-- ******************************************************************************************************--
function GenerateTemplate(pT, tblCollections)
local Collection = fhGetItemText(pT, '~.COLL')
local Name = fhGetItemText(pT, '~.NAME')
-- build up new Template definition
local tblT = {''}
table.insert(tblT, WriteXMLline(pT, 'NAME', 'Name'))
table.insert(tblT, WriteXMLline(pT, 'CATG', 'Category'))
table.insert(tblT, WriteXMLline(pT, 'SUBC', 'Subcategory'))
table.insert(tblT, WriteXMLline(pT, 'DESC', 'Description'))
table.insert(tblT, WriteXMLline(pT, 'TITL', 'Record_Title'))
table.insert(tblT, WriteXMLline(pT, 'FOOT', 'Footnote'))
table.insert(tblT, WriteXMLline(pT, 'SHRT', 'Short_Footnote'))
table.insert(tblT, WriteXMLline(pT, 'BIBL', 'Bibliography'))
local pF = fhNewItemPtr()
pF:MoveTo(pT, '~.FDEF')
while pF:IsNotNull() do
table.insert(tblT, '')
table.insert(tblT, WriteXMLline(pF, 'NAME', 'Name'))
table.insert(tblT, WriteXMLline(pF, 'CODE', 'Code'))
table.insert(tblT, WriteXMLline(pF, 'TYPE', 'Type'))
table.insert(tblT, WriteXMLline(pF, 'PROM', 'Prompt'))
table.insert(tblT, WriteXMLline(pF, 'DESC', 'Description'))
if fhGetItemPtr(pF, '~.CITN'):IsNotNull() then table.insert(tblT, 'Yes ') end
table.insert(tblT, ' ')
pF:MoveNext('SAME_TAG')
end
table.insert(tblT, '')
-- update master collection list
if not tblCollections[Collection] then tblCollections[Collection] = {} end
tblCollections[Collection][Name] = tblT
end
-- ******************************************************************************************************--
function WriteXMLline(pParent, Tag, XMLtag)
local p = fhGetItemPtr(pParent, '~.' .. Tag)
if p:IsNotNull() then
local Text = fhGetValueAsText(p)
Text = Text:gsub('&', '&'):gsub('<', '<'):gsub('>', '>')
local XML = '<' .. XMLtag .. '>' .. Text .. '' .. XMLtag .. '>'
return XML
else
local XML = '<' .. XMLtag .. '/>'
return XML
end
end
-- ******************************************************************************************************--
function UpdateDefinitionFile(Collection, tblCollections, tblHeaders)
local Path = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Source Templates\\Custom'
local File = Path .. '\\' .. Collection .. '.fhst'
local tblT = {}
if tblHeaders[Collection] then
tblT = tblHeaders[Collection]
else
tblT = {'', ''}
end
for _, Template in pairs(tblCollections[Collection]) do
for _, Line in ipairs(Template) do
table.insert(tblT, Line)
end
end
table.insert(tblT, ' \n')
fhSaveTextFile(File, table.concat(tblT, '\n'))
end
-- ******************************************************************************************************--
function MessageBox(Message, Buttons, Icon, Title, Default)
-- replaces built-in function with custom version containing more options
local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
title = Title or 'Create Source Template Definitions (1.2)', buttondefault = Default}
msgdlg:popup()
return tonumber(msgdlg.ButtonResponse)
end
-- ******************************************************************************************************--
main()
--[[
@Title: Create Source Template Definitions
@Type: Standard
@Author: Mark Draper
@Version: 1.2
@LastUpdated: 28 Apr 2025
@Licence: This plugin is copyright (c) 2025 Mark Draper and is licensed under the MIT License which
is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Creates a Template Definition for user-defined or imported Source Templates where this does
not exist already, and saves it to the relevant Family Historian Source Templates collection.
This enables the definitions to be used in other Projects.
]]
--[[
Version 1.2 (Apr 2025)
- Multi-monitor support added (dedicated form rather than simple iup alarmm)
]]
fhInitialise(7,0,0, 'save_recommended')
fhu = require('fhUtils')
fhu.setIupDefaults()
function main()
local pT = fhNewItemPtr()
pT:MoveToFirstRecord('_SRCT')
-- check if project is using templates
if pT:IsNull() then
MessageBox('This project does not use Templated Sources.', 'OK', 'ERROR')
return
end
-- check for duplicate names
if CheckDuplicates() then return end
-- read in available template definitions
local tblCollections, tblHeaders = Initialize()
-- check Project templates for missing definitions and links
local tblMissing = {}
local tblX = {}
while pT:IsNotNull() do
if MissingTemplate(pT, tblCollections) then
table.insert(tblMissing, pT:Clone())
end
if fhCallBuiltInFunction('LinksTo', pT) == 0 then table.insert(tblX, pT:Clone()) end
pT:MoveNext()
end
if #tblMissing == 0 then
local Msg = 'All validated Source Templates have a corresponding definition.\n\n' ..
'Select "Tools>Source Template Definitions from the main menu to check that ' ..
'they are fully synchronized.'
MessageBox(Msg, 'OK', 'INFORMATION')
return
end
local Msg = '\nThere are ' .. #tblMissing .. ' Source Templates in this Project without a ' ..
'corresponding Source Template Definition.\n\n'
if #tblMissing == 1 then Msg = '\nThere is 1 Source Template in this Project without a ' ..
'corresponding Source Template Definition.\n\n' end
repeat
local Option = AlarmForm('Create Source Template Definitions (1.2)', Msg,
'Create Definitions', 'Help', 'Close')
if Option == 2 then
local H = 'https://pluginstore.family-historian.co.uk/help/create-source-template-definitions'
fhShellExecute(H) -- keep line length below self-imposed style limit
elseif Option == 0 or Option == 3 then return end
until Option == 1
-- ask for permission to make irreversible changes
Msg = 'Updating Source Template Definitions cannot be reversed by using Edit > Undo Plugin Updates. ' ..
'Undoing the changes will require restoring the relevant template definition file(s) from a ' ..
'back up (see the plugin help for more details on where definition files are stored).\n\n' ..
'Are you sure that you want to proceed?'
if MessageBox(Msg, 'OKCANCEL', 'WARNING', nil, 2) ~= 1 then return end
-- work through missing template definitions
local tblUpdated = {} -- new or updated collections
for _, Template in ipairs(tblMissing) do
GenerateTemplate(Template, tblCollections)
tblUpdated[fhGetItemText(Template, '~.COLL')] = true
end
for Collection, _ in pairs(tblUpdated) do
UpdateDefinitionFile(Collection, tblCollections, tblHeaders)
end
local Msg = 'Template update complete.\n\nSelect "Tools>Source Template Definitions..." from the ' ..
'main menu to check that they are fully synchronized.'
if #tblX > 0 then Msg = Msg .. '\n\n' .. #tblX .. ' Source Templates have no linked sources. ' ..
'Do you want to delete these from your project?'
if MessageBox(Msg, 'YESNO', 'QUESTION', nil, 2) == 1 then
for _, Template in ipairs(tblX) do fhDeleteItem(Template) end
fhUpdateDisplay()
end
else
MessageBox(Msg, 'OK', 'INFORMATION')
end
end
-- ******************************************************************************************************--
function AlarmForm(formtitle, heading, txt1, txt2, txt3)
-- create form modelled on iup.alarm (full form supports parent in multi-monitor setup)
local action = 0
local btn1 = iup.button{title = txt1, padding = '10x3',
action = function(self) action = 1 return iup.CLOSE end}
local btn2 = iup.button{title = txt2, padding = '10x3',
action = function(self) action = 2 return iup.CLOSE end}
local btn3 = iup.button{title = txt3, padding = '10x3',
action = function(self) action = 3 return iup.CLOSE end}
local buttons = iup.hbox{iup.fill{}, btn1, btn2, btn3, iup.fill{};
margin = '0x10', normalizesize = 'Both', padding = 10, gap = 20}
local label = iup.label{title = heading}
local vbox = iup.vbox{label, buttons; margin = '20x20'}
local dialog = iup.dialog{vbox; title = formtitle, resize = 'No', minbox = 'No'}
if fhGetAppVersion() > 6 then
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
end
dialog:popup()
return action
end
-- ******************************************************************************************************--
function CheckDuplicates()
-- checks for duplicated Template name
local tblT = {}
local pT = fhNewItemPtr()
pT:MoveToFirstRecord('_SRCT')
while pT:IsNotNull() do
local Name = fhGetItemText(pT, '~.COLL') .. fhGetItemText(pT, '~.NAME')
if tblT[Name] then
local Msg = 'There are multiple Templates called ' .. fhGetItemText(pT, '~.NAME') .. ' in ' ..
'the ' .. fhGetItemText(pT, '~.COLL') .. ' Collection. Please ensure that Templates ' ..
'have a unique name within their Collection prior to creating the Template Definitions.'
MessageBox(Msg, 'OK', 'ERROR')
return true
end
tblT[Name] = true
pT:MoveNext()
end
end
-- ******************************************************************************************************--
function Initialize()
--[[
Builds up a table of Template Collections.
tblD is a table of Template definitions (Collection name => table of Templates in that Collection).
tblH is a table of header information (Collection name => table of XML lines prior to
the Template definitions in the Collection file).
This method enforces unique Template names within a given Collection, although FH permits duplicate names.
]]
local field
local Path = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Source Templates\\Custom'
local TemplateName
local tblD = {}
local tblH = {}
local FSO = luacom.CreateObject('Scripting.FileSystemObject')
local objFolder = FSO:GetFolder(Path)
for _, File in luacom.pairs(objFolder.Files) do
if File.Name:match('.fhst$') then
local Collection = File.Name:sub(1, -6) -- remove extension
local FileContents = fhLoadTextFile(File.Path)
if FileContents then
local tblC = {}
local tblT = {}
local header = true
for line in FileContents:gmatch('[^\r\n]+') do
if line == '' then
if header then tblH[Collection] = tblT end -- end of file header
tblT = {}
header = false
field = false
elseif line == '' then
field = true
end
table.insert(tblT, line)
local _, T, _ = line:match('^()(.+)( )$')
if T and not field then
TemplateName = T:gsub('&', '&'):gsub('<', '<'):gsub('>', '>')
end
if line == ' ' then
tblC[TemplateName] = tblT -- save completed template
tblT = {} -- clear table
end
end
tblD[Collection] = tblC -- save collection
end
end
end
return tblD, tblH
end
-- ******************************************************************************************************--
function MissingTemplate(pT, tblCollections)
local Collection = fhGetItemText(pT, '~.COLL')
local Name = fhGetItemText(pT, '~.NAME')
if Name == '' then
MessageBox('Unnamed Template definitions cannot be exported', 'OK', 'WARNING')
return
elseif Collection == '' then
MessageBox('Source Template Record ' .. Name .. ' does not have a defined ' ..
'Collection, so cannot be exported.', 'OK', 'WARNING')
return
elseif Collection == 'Essentials' or Collection == 'Advanced' then
return
elseif tblCollections[Collection] and tblCollections[Collection][Name] then
return
else
return true
end
end
-- ******************************************************************************************************--
function GenerateTemplate(pT, tblCollections)
local Collection = fhGetItemText(pT, '~.COLL')
local Name = fhGetItemText(pT, '~.NAME')
-- build up new Template definition
local tblT = {''}
table.insert(tblT, WriteXMLline(pT, 'NAME', 'Name'))
table.insert(tblT, WriteXMLline(pT, 'CATG', 'Category'))
table.insert(tblT, WriteXMLline(pT, 'SUBC', 'Subcategory'))
table.insert(tblT, WriteXMLline(pT, 'DESC', 'Description'))
table.insert(tblT, WriteXMLline(pT, 'TITL', 'Record_Title'))
table.insert(tblT, WriteXMLline(pT, 'FOOT', 'Footnote'))
table.insert(tblT, WriteXMLline(pT, 'SHRT', 'Short_Footnote'))
table.insert(tblT, WriteXMLline(pT, 'BIBL', 'Bibliography'))
local pF = fhNewItemPtr()
pF:MoveTo(pT, '~.FDEF')
while pF:IsNotNull() do
table.insert(tblT, '')
table.insert(tblT, WriteXMLline(pF, 'NAME', 'Name'))
table.insert(tblT, WriteXMLline(pF, 'CODE', 'Code'))
table.insert(tblT, WriteXMLline(pF, 'TYPE', 'Type'))
table.insert(tblT, WriteXMLline(pF, 'PROM', 'Prompt'))
table.insert(tblT, WriteXMLline(pF, 'DESC', 'Description'))
if fhGetItemPtr(pF, '~.CITN'):IsNotNull() then table.insert(tblT, 'Yes ') end
table.insert(tblT, ' ')
pF:MoveNext('SAME_TAG')
end
table.insert(tblT, '')
-- update master collection list
if not tblCollections[Collection] then tblCollections[Collection] = {} end
tblCollections[Collection][Name] = tblT
end
-- ******************************************************************************************************--
function WriteXMLline(pParent, Tag, XMLtag)
local p = fhGetItemPtr(pParent, '~.' .. Tag)
if p:IsNotNull() then
local Text = fhGetValueAsText(p)
Text = Text:gsub('&', '&'):gsub('<', '<'):gsub('>', '>')
local XML = '<' .. XMLtag .. '>' .. Text .. '' .. XMLtag .. '>'
return XML
else
local XML = '<' .. XMLtag .. '/>'
return XML
end
end
-- ******************************************************************************************************--
function UpdateDefinitionFile(Collection, tblCollections, tblHeaders)
local Path = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Source Templates\\Custom'
local File = Path .. '\\' .. Collection .. '.fhst'
local tblT = {}
if tblHeaders[Collection] then
tblT = tblHeaders[Collection]
else
tblT = {'', ''}
end
for _, Template in pairs(tblCollections[Collection]) do
for _, Line in ipairs(Template) do
table.insert(tblT, Line)
end
end
table.insert(tblT, ' \n')
fhSaveTextFile(File, table.concat(tblT, '\n'))
end
-- ******************************************************************************************************--
function MessageBox(Message, Buttons, Icon, Title, Default)
-- replaces built-in function with custom version containing more options
local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
title = Title or 'Create Source Template Definitions (1.2)', buttondefault = Default}
msgdlg:popup()
return tonumber(msgdlg.ButtonResponse)
end
-- ******************************************************************************************************--
main()Source:Create-Source-Template-Definitions-1.fh_lua