Tabulate Source Template Definitions.fh_lua--[[
@Title: Tabulate Source Template Definitions
@Type: Standard
@Author: Mark Draper
@Version: 1.1
@LastUpdated: 13 Jan 2026
@Licence: This plugin is copyright (c) 2026 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: Tablulates Source Template Definitions
]]
--[[
Version 1.0 (Feb 2024)
- Initial Plugin Store version
Version 1.1 (Jan 2026)
- Extended desktop compatibility added
]]
fhInitialise(7)
fh = require('fhUtils')
fh.setIupDefaults()
FSO = luacom.CreateObject('Scripting.FileSystemObject')
-- ************************************************************************** --
function main()
-- define tables of Collections, Template and Data fields
local tblCollections = GetCollections()
local tblFields = {'Name', 'Category', 'Subcategory', 'Collection', 'Description', 'Record Title Format',
'Bibliography Format', 'Footnote Format', 'Short Footnote Format'}
local tblDataFields = {'Name', 'Type', 'Prompt', 'Description', 'Citation'}
-- present user menu
local tblC, tblF, tblDF, tblCsel, tblFsel, tblDFsel
repeat
tblC, tblF, tblDF = Menu(tblCollections, tblFields, tblDataFields)
if not tblC then return end -- user cancelled
tblCsel = {}
for _, C in ipairs(tblC) do
if C.Value == 'ON' then table.insert(tblCsel, C.FileName) end
end
tblFsel = {}
for _, F in ipairs(tblF) do
if F.Value == 'ON' then tblFsel[F.Title] = true end
end
tblDFsel = {}
for _, DF in ipairs(tblDF) do
if DF.Value == 'ON' then tblDFsel[DF.Title] = true end
end
if #tblCsel == 0 or TableSize(tblFsel) == 0 then
fhMessageBox('Nothing to display.', 'MB_OK', 'MB_ICONEXCLAMATION')
else
break -- ready to process
end
until false
-- read selected Collection files
local tblTemplates = {} -- table for template data
for _, Collection in ipairs(tblCsel) do
GetTemplates(Collection, tblTemplates)
end
SaveOptions(tblC, tblF, tblDF)
-- output results
DisplayData(tblTemplates, tblFsel, tblDFsel)
end
-- ************************************************************************** --
function Menu(tblCollections, tblFields, tblDataFields)
local ok
local OptionsFile = fhGetPluginDataFileName('LOCAL_MACHINE'):gsub('.dat', '.ini')
local tblC = {}
local vboxC = iup.vbox{gap = 5, margin = '10x10'}
for _, C in ipairs(tblCollections) do
local optC = iup.toggle{title = C:match('([^\\]+).fhst$'), expand = 'HORIZONTAL',
value = 'ON', FileName = C}
if FSO:FileExists(OptionsFile) and not
fhGetIniFileValue(OptionsFile, 'Collections', C, 'bool', false) then
optC.Value = 'OFF'
end
table.insert(tblC, optC)
iup.Append(vboxC, optC)
end
local fraC = iup.frame{vboxC; title = 'Collections'}
local tblF = {}
local vboxF = iup.vbox{gap = 5, margin = '10x10'}
for _, F in ipairs(tblFields) do
local optF = iup.toggle{title = F, expand = 'HORIZONTAL', value = 'ON'}
if FSO:FileExists(OptionsFile) and not
fhGetIniFileValue(OptionsFile, 'Template Fields', F, 'bool', false) then
optF.Value = 'OFF'
end
table.insert(tblF, optF)
iup.Append(vboxF, optF)
end
local fraF = iup.frame{vboxF; title = 'Template Fields'}
local tblDF = {}
local vboxDF = iup.vbox{gap = 5, margin = '10x10'}
for _, DF in ipairs(tblDataFields) do
local optDF = iup.toggle{title = DF, expand = 'HORIZONTAL', value = 'ON'}
if FSO:FileExists(OptionsFile) and not
fhGetIniFileValue(OptionsFile, 'Template Data Fields', DF, 'bool', false) then
optDF.Value = 'OFF'
end
table.insert(tblDF, optDF)
iup.Append(vboxDF, optDF)
end
local fraDF = iup.frame{vboxDF; title = 'Data Fields'}
local btnSelectAll = iup.button{title = 'Select All', padding = '10x3',
action = function(self)
for _, tbl in ipairs({tblC, tblF, tblDF}) do
for _, t in ipairs(tbl) do t.Value = 'ON' end
end end}
local btnClearAll = iup.button{title = 'Clear All',
action = function(self)
for _, tbl in ipairs({tblC, tblF, tblDF}) do
for _, t in ipairs(tbl) do t.Value = 'OFF' end
end end}
local btnOK = iup.button{title = 'OK',
action = function(self) ok = true return iup.CLOSE end}
local btnClose = iup.button{title = 'Close',
action = function(self) return iup.CLOSE end}
local hboxButtons = iup.hbox{btnOK, btnClose, btnSelectAll, btnClearAll;
gap = 40, margin = 'x25', normalizesize = 'BOTH'}
local hbox = iup.hbox{fraC, fraF, fraDF; gap = 10, margin = '10x10'}
local vbox = iup.vbox{hbox, hboxButtons; alignment = 'ACENTER', margin = '20x'}
local dialog = iup.dialog{vbox; resize = 'NO', minbox = 'NO', maxbox = 'NO',
title = 'Tabulate Source Template Definitions (1.1)'}
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
dialog:popup()
if ok then return tblC, tblF, tblDF end
end
-- ************************************************************************** --
function GetCollections()
local DataFolder = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Source Templates\\'
local tblCollections = {}
local objFolder = FSO:GetFolder(DataFolder .. 'Custom')
for _, File in luacom.pairs(objFolder.Files) do
if File.Path:match('.fhst$') then
table.insert(tblCollections, File.Path)
end
end
table.sort(tblCollections)
if FSO:FileExists(DataFolder .. 'Standard\\Advanced.fhst') then
table.insert(tblCollections, 1, DataFolder .. 'Standard\\Advanced.fhst')
end
if FSO:FileExists(DataFolder .. 'Standard\\Essentials.fhst') then
table.insert(tblCollections, 1, DataFolder .. 'Standard\\Essentials.fhst')
end
return tblCollections
end
-- ************************************************************************** --
function GetTemplates(File, tblTemplates)
local tblTemplate = {} -- data for single template
local tblField = {} -- data for single data field
local tblFields = {} -- table of all data fields
local Flag -- reading template or data field?
local S = fhLoadTextFile(File)
for line in S:gmatch('[^\r\n]+') do
if line == '' then -- start of new template
tblTemplate = {}
tblFields = {}
tblTemplate.Collection = File:match('([^\\]+).fhst$')
Flag = 'T'
elseif line == '' then -- end of template
tblTemplate.Fields = tblFields
table.insert(tblTemplates, tblTemplate)
Flag = nil
elseif line == '' then -- start of new data field
tblField = {}
Flag = 'F'
elseif line == ' ' then -- end of data field
table.insert(tblFields, tblField)
Flag = nil
else
local Tag, Text, Endtag = line:match('^(<.+>)(.+)()$')
if Tag and Endtag and Tag:sub(2) == Endtag:sub(3) and Endtag:sub(2, 2) == '/' then
Text = Text:gsub('&', '&'):gsub('<', '<'):gsub('>', '>')
Tag = Tag:match('^<(.+)>$') -- remove angled brackets
if Flag and Flag == 'T' then
tblTemplate[Tag] = Text
elseif Flag and Flag == 'F' then
tblField[Tag] = Text
end
end
end
end
end
-- ************************************************************************** --
function DisplayData(tblTemplates, tblFsel, tblDFsel)
local tblColl = {}
local tblName = {}
local tblCat = {}
local tblSubCat = {}
local tblDesc = {}
local tblRecTitle = {}
local tblFoot = {}
local tblSFoot = {}
local tblBibl = {}
local tblDFN = {}
local tblDFT = {}
local tblDFD = {}
local tblDFC = {}
local tblDFP = {}
for _, Template in ipairs(tblTemplates) do
local DF = 1 -- counter for data fields
while true do
table.insert(tblColl, Template.Collection or '')
table.insert(tblName, Template.Name or '')
table.insert(tblCat, Template.Category or '')
table.insert(tblSubCat, Template.Subcategory or '')
table.insert(tblDesc, Template.Description or '')
table.insert(tblRecTitle, Template.Record_Title or '')
table.insert(tblFoot, Template.Footnote or '')
table.insert(tblSFoot, Template.Short_Footnote or '')
table.insert(tblBibl, Template.Bibliography or '')
if DF <= #Template.Fields and TableSize(tblDFsel) > 0 then -- process data field
table.insert(tblDFN, Template.Fields[DF].Name or '')
table.insert(tblDFT, Template.Fields[DF].Type or '')
table.insert(tblDFD, Template.Fields[DF].Description or '')
table.insert(tblDFC, Template.Fields[DF].Citation or '')
table.insert(tblDFP, Template.Fields[DF].Prompt or '')
DF = DF + 1
end
if DF > #Template.Fields or TableSize(tblDFsel) == 0 then break end
end
end
local N = #tblColl -- not strictly necessary, but keeps lines shorter!
fhOutputResultSetTitles('Source Template Definitions')
if tblFsel.Collection then
fhOutputResultSetColumn('Collection', 'text', tblColl, N, 80, 'align_left', 1) end
if tblFsel.Name then
fhOutputResultSetColumn('Name', 'text', tblName, N, 180, 'align_left', 2) end
if tblFsel.Category then
fhOutputResultSetColumn('Category', 'text', tblCat, N) end
if tblFsel.Subcategory then
fhOutputResultSetColumn('Subcategory', 'text', tblSubCat, N) end
if tblFsel.Description then
fhOutputResultSetColumn('Description', 'text', tblDesc, N, 180) end
if tblFsel['Record Title Format'] then
fhOutputResultSetColumn('Record Title Format', 'text', tblRecTitle, N, 180) end
if tblFsel['Bibliography Format'] then
fhOutputResultSetColumn('Bibliography Format', 'text', tblBibl, N, 180) end
if tblFsel['Footnote Format'] then
fhOutputResultSetColumn('Footnote Format', 'text', tblFoot, N, 180) end
if tblFsel['Short Footnote Format'] then
fhOutputResultSetColumn('Short Footnote Format', 'text', tblSFoot, N, 180) end
if tblDFsel.Name then
fhOutputResultSetColumn('Field Name', 'text', tblDFN, N) end
if tblDFsel.Type then
fhOutputResultSetColumn('Field Type', 'text', tblDFT, N, 40) end
if tblDFsel.Prompt then
fhOutputResultSetColumn('Field Prompt', 'text', tblDFP, N, 180) end
if tblDFsel.Description then
fhOutputResultSetColumn('Field Description', 'text', tblDFD, N) end
if tblDFsel.Citation then
fhOutputResultSetColumn('Citation', 'text', tblDFC, N, 40) end
end
-- ************************************************************************** --
function SaveOptions(tblC, tblF, tblDF)
local File = fhGetPluginDataFileName('LOCAL_MACHINE'):gsub('.dat', '.ini')
fhSaveTextFile(File, '', 'UTF-16LE') -- ensures Unicode ini file
for _, C in ipairs(tblC) do
fhSetIniFileValue(File, 'Collections', C.FileName, 'bool', C.Value == 'ON')
end
for _, F in ipairs(tblF) do
fhSetIniFileValue(File, 'Template Fields', F.Title, 'bool', F.Value == 'ON')
end
for _, DF in ipairs(tblDF) do
fhSetIniFileValue(File, 'Template Data Fields', DF.Title, 'bool', DF.Value == 'ON')
end
end
-- ************************************************************************** --
function TableSize(tblT)
local n = 0
for _ in pairs(tblT) do n = n + 1 end
return n
end
-- ************************************************************************** --
main()
--[[
@Title: Tabulate Source Template Definitions
@Type: Standard
@Author: Mark Draper
@Version: 1.1
@LastUpdated: 13 Jan 2026
@Licence: This plugin is copyright (c) 2026 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: Tablulates Source Template Definitions
]]
--[[
Version 1.0 (Feb 2024)
- Initial Plugin Store version
Version 1.1 (Jan 2026)
- Extended desktop compatibility added
]]
fhInitialise(7)
fh = require('fhUtils')
fh.setIupDefaults()
FSO = luacom.CreateObject('Scripting.FileSystemObject')
-- ************************************************************************** --
function main()
-- define tables of Collections, Template and Data fields
local tblCollections = GetCollections()
local tblFields = {'Name', 'Category', 'Subcategory', 'Collection', 'Description', 'Record Title Format',
'Bibliography Format', 'Footnote Format', 'Short Footnote Format'}
local tblDataFields = {'Name', 'Type', 'Prompt', 'Description', 'Citation'}
-- present user menu
local tblC, tblF, tblDF, tblCsel, tblFsel, tblDFsel
repeat
tblC, tblF, tblDF = Menu(tblCollections, tblFields, tblDataFields)
if not tblC then return end -- user cancelled
tblCsel = {}
for _, C in ipairs(tblC) do
if C.Value == 'ON' then table.insert(tblCsel, C.FileName) end
end
tblFsel = {}
for _, F in ipairs(tblF) do
if F.Value == 'ON' then tblFsel[F.Title] = true end
end
tblDFsel = {}
for _, DF in ipairs(tblDF) do
if DF.Value == 'ON' then tblDFsel[DF.Title] = true end
end
if #tblCsel == 0 or TableSize(tblFsel) == 0 then
fhMessageBox('Nothing to display.', 'MB_OK', 'MB_ICONEXCLAMATION')
else
break -- ready to process
end
until false
-- read selected Collection files
local tblTemplates = {} -- table for template data
for _, Collection in ipairs(tblCsel) do
GetTemplates(Collection, tblTemplates)
end
SaveOptions(tblC, tblF, tblDF)
-- output results
DisplayData(tblTemplates, tblFsel, tblDFsel)
end
-- ************************************************************************** --
function Menu(tblCollections, tblFields, tblDataFields)
local ok
local OptionsFile = fhGetPluginDataFileName('LOCAL_MACHINE'):gsub('.dat', '.ini')
local tblC = {}
local vboxC = iup.vbox{gap = 5, margin = '10x10'}
for _, C in ipairs(tblCollections) do
local optC = iup.toggle{title = C:match('([^\\]+).fhst$'), expand = 'HORIZONTAL',
value = 'ON', FileName = C}
if FSO:FileExists(OptionsFile) and not
fhGetIniFileValue(OptionsFile, 'Collections', C, 'bool', false) then
optC.Value = 'OFF'
end
table.insert(tblC, optC)
iup.Append(vboxC, optC)
end
local fraC = iup.frame{vboxC; title = 'Collections'}
local tblF = {}
local vboxF = iup.vbox{gap = 5, margin = '10x10'}
for _, F in ipairs(tblFields) do
local optF = iup.toggle{title = F, expand = 'HORIZONTAL', value = 'ON'}
if FSO:FileExists(OptionsFile) and not
fhGetIniFileValue(OptionsFile, 'Template Fields', F, 'bool', false) then
optF.Value = 'OFF'
end
table.insert(tblF, optF)
iup.Append(vboxF, optF)
end
local fraF = iup.frame{vboxF; title = 'Template Fields'}
local tblDF = {}
local vboxDF = iup.vbox{gap = 5, margin = '10x10'}
for _, DF in ipairs(tblDataFields) do
local optDF = iup.toggle{title = DF, expand = 'HORIZONTAL', value = 'ON'}
if FSO:FileExists(OptionsFile) and not
fhGetIniFileValue(OptionsFile, 'Template Data Fields', DF, 'bool', false) then
optDF.Value = 'OFF'
end
table.insert(tblDF, optDF)
iup.Append(vboxDF, optDF)
end
local fraDF = iup.frame{vboxDF; title = 'Data Fields'}
local btnSelectAll = iup.button{title = 'Select All', padding = '10x3',
action = function(self)
for _, tbl in ipairs({tblC, tblF, tblDF}) do
for _, t in ipairs(tbl) do t.Value = 'ON' end
end end}
local btnClearAll = iup.button{title = 'Clear All',
action = function(self)
for _, tbl in ipairs({tblC, tblF, tblDF}) do
for _, t in ipairs(tbl) do t.Value = 'OFF' end
end end}
local btnOK = iup.button{title = 'OK',
action = function(self) ok = true return iup.CLOSE end}
local btnClose = iup.button{title = 'Close',
action = function(self) return iup.CLOSE end}
local hboxButtons = iup.hbox{btnOK, btnClose, btnSelectAll, btnClearAll;
gap = 40, margin = 'x25', normalizesize = 'BOTH'}
local hbox = iup.hbox{fraC, fraF, fraDF; gap = 10, margin = '10x10'}
local vbox = iup.vbox{hbox, hboxButtons; alignment = 'ACENTER', margin = '20x'}
local dialog = iup.dialog{vbox; resize = 'NO', minbox = 'NO', maxbox = 'NO',
title = 'Tabulate Source Template Definitions (1.1)'}
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
dialog:popup()
if ok then return tblC, tblF, tblDF end
end
-- ************************************************************************** --
function GetCollections()
local DataFolder = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Source Templates\\'
local tblCollections = {}
local objFolder = FSO:GetFolder(DataFolder .. 'Custom')
for _, File in luacom.pairs(objFolder.Files) do
if File.Path:match('.fhst$') then
table.insert(tblCollections, File.Path)
end
end
table.sort(tblCollections)
if FSO:FileExists(DataFolder .. 'Standard\\Advanced.fhst') then
table.insert(tblCollections, 1, DataFolder .. 'Standard\\Advanced.fhst')
end
if FSO:FileExists(DataFolder .. 'Standard\\Essentials.fhst') then
table.insert(tblCollections, 1, DataFolder .. 'Standard\\Essentials.fhst')
end
return tblCollections
end
-- ************************************************************************** --
function GetTemplates(File, tblTemplates)
local tblTemplate = {} -- data for single template
local tblField = {} -- data for single data field
local tblFields = {} -- table of all data fields
local Flag -- reading template or data field?
local S = fhLoadTextFile(File)
for line in S:gmatch('[^\r\n]+') do
if line == '' then -- start of new template
tblTemplate = {}
tblFields = {}
tblTemplate.Collection = File:match('([^\\]+).fhst$')
Flag = 'T'
elseif line == '' then -- end of template
tblTemplate.Fields = tblFields
table.insert(tblTemplates, tblTemplate)
Flag = nil
elseif line == '' then -- start of new data field
tblField = {}
Flag = 'F'
elseif line == ' ' then -- end of data field
table.insert(tblFields, tblField)
Flag = nil
else
local Tag, Text, Endtag = line:match('^(<.+>)(.+)()$')
if Tag and Endtag and Tag:sub(2) == Endtag:sub(3) and Endtag:sub(2, 2) == '/' then
Text = Text:gsub('&', '&'):gsub('<', '<'):gsub('>', '>')
Tag = Tag:match('^<(.+)>$') -- remove angled brackets
if Flag and Flag == 'T' then
tblTemplate[Tag] = Text
elseif Flag and Flag == 'F' then
tblField[Tag] = Text
end
end
end
end
end
-- ************************************************************************** --
function DisplayData(tblTemplates, tblFsel, tblDFsel)
local tblColl = {}
local tblName = {}
local tblCat = {}
local tblSubCat = {}
local tblDesc = {}
local tblRecTitle = {}
local tblFoot = {}
local tblSFoot = {}
local tblBibl = {}
local tblDFN = {}
local tblDFT = {}
local tblDFD = {}
local tblDFC = {}
local tblDFP = {}
for _, Template in ipairs(tblTemplates) do
local DF = 1 -- counter for data fields
while true do
table.insert(tblColl, Template.Collection or '')
table.insert(tblName, Template.Name or '')
table.insert(tblCat, Template.Category or '')
table.insert(tblSubCat, Template.Subcategory or '')
table.insert(tblDesc, Template.Description or '')
table.insert(tblRecTitle, Template.Record_Title or '')
table.insert(tblFoot, Template.Footnote or '')
table.insert(tblSFoot, Template.Short_Footnote or '')
table.insert(tblBibl, Template.Bibliography or '')
if DF <= #Template.Fields and TableSize(tblDFsel) > 0 then -- process data field
table.insert(tblDFN, Template.Fields[DF].Name or '')
table.insert(tblDFT, Template.Fields[DF].Type or '')
table.insert(tblDFD, Template.Fields[DF].Description or '')
table.insert(tblDFC, Template.Fields[DF].Citation or '')
table.insert(tblDFP, Template.Fields[DF].Prompt or '')
DF = DF + 1
end
if DF > #Template.Fields or TableSize(tblDFsel) == 0 then break end
end
end
local N = #tblColl -- not strictly necessary, but keeps lines shorter!
fhOutputResultSetTitles('Source Template Definitions')
if tblFsel.Collection then
fhOutputResultSetColumn('Collection', 'text', tblColl, N, 80, 'align_left', 1) end
if tblFsel.Name then
fhOutputResultSetColumn('Name', 'text', tblName, N, 180, 'align_left', 2) end
if tblFsel.Category then
fhOutputResultSetColumn('Category', 'text', tblCat, N) end
if tblFsel.Subcategory then
fhOutputResultSetColumn('Subcategory', 'text', tblSubCat, N) end
if tblFsel.Description then
fhOutputResultSetColumn('Description', 'text', tblDesc, N, 180) end
if tblFsel['Record Title Format'] then
fhOutputResultSetColumn('Record Title Format', 'text', tblRecTitle, N, 180) end
if tblFsel['Bibliography Format'] then
fhOutputResultSetColumn('Bibliography Format', 'text', tblBibl, N, 180) end
if tblFsel['Footnote Format'] then
fhOutputResultSetColumn('Footnote Format', 'text', tblFoot, N, 180) end
if tblFsel['Short Footnote Format'] then
fhOutputResultSetColumn('Short Footnote Format', 'text', tblSFoot, N, 180) end
if tblDFsel.Name then
fhOutputResultSetColumn('Field Name', 'text', tblDFN, N) end
if tblDFsel.Type then
fhOutputResultSetColumn('Field Type', 'text', tblDFT, N, 40) end
if tblDFsel.Prompt then
fhOutputResultSetColumn('Field Prompt', 'text', tblDFP, N, 180) end
if tblDFsel.Description then
fhOutputResultSetColumn('Field Description', 'text', tblDFD, N) end
if tblDFsel.Citation then
fhOutputResultSetColumn('Citation', 'text', tblDFC, N, 40) end
end
-- ************************************************************************** --
function SaveOptions(tblC, tblF, tblDF)
local File = fhGetPluginDataFileName('LOCAL_MACHINE'):gsub('.dat', '.ini')
fhSaveTextFile(File, '', 'UTF-16LE') -- ensures Unicode ini file
for _, C in ipairs(tblC) do
fhSetIniFileValue(File, 'Collections', C.FileName, 'bool', C.Value == 'ON')
end
for _, F in ipairs(tblF) do
fhSetIniFileValue(File, 'Template Fields', F.Title, 'bool', F.Value == 'ON')
end
for _, DF in ipairs(tblDF) do
fhSetIniFileValue(File, 'Template Data Fields', DF.Title, 'bool', DF.Value == 'ON')
end
end
-- ************************************************************************** --
function TableSize(tblT)
local n = 0
for _ in pairs(tblT) do n = n + 1 end
return n
end
-- ************************************************************************** --
main()Source:Tabulate-Source-Template-Definitions-1.fh_lua