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	-- 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