Lumped Source Splitter.fh_lua--[[
@Title: Lumped Source Splitter
@Type: Standard
@Author: Mark Draper
@Version: 1.5
@LastUpdated: 25 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: Converts a lumped source to individual split sources. Citations are regarded as equivalent
if they have identical Where within Source and Text from Source (or Citation level fields for Templated
Sources), and the same attached Media or Note Records. All of these items are moved to the new split
source and the original lumped citation deleted.
]]
--[[
Version 1.2 (Oct 2021)
- Initial Plugin Store version, equivalent to FHUG version 1.1.3
Version 1.3 (Jun 2023)
- Redesigned treatment of templated sources, moving citation data to source level
- Improved messaging and reporting
- Numerous improvements in coding to speed up execution time
Version 1.4 (Jul 2023)
- Citation notes now ignored for splitting
- Minor menu presentation improvements
Version 1.4.1 (Feb 2024)
- Corrects bug whereby media are retained in citation
Version 1.5 (Apr 2025)
- Support for multiple monitors added
- Support for enhanced tooltips added
]]
fhInitialise(7, 0, 15, 'save_recommended')
require('iuplua')
require('iupluaimglib')
fhu = require('fhUtils')
fhu.setIupDefaults()
iup.SetGlobal('IMAGESTOCKSIZE', 32)
-- *********************************************************************
function main()
gblReport = {} -- initialize table for plugin results
local selection
local msgTitle = 'This plugin converts a lumped source into individual split sources and recombines\n' ..
'duplicated citations created during import from another genealogy application'
local lblTitle = iup.label{title = msgTitle}
local btnSingle = iup.button{title = 'Single Source', padding = '10x3',
tip = 'Select a single source for splitting'}
local btnMultiple = iup.button{title = 'Multiple Sources',
tip = 'Select multiple sources for splitting'}
local btnTemplate = iup.button{title = 'Split by Template',
tip = 'Split all sources with same template'}
local btnHelp = iup.button{title = 'Help',
action = function(self) fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/lumped-source-splitter') end,
tip = 'Show plugin help page'}
local btnClose = iup.button{title = 'Close',
action = function(self) return iup.CLOSE end, padding = '10x3',
tip = 'Close plugin'}
-- define enhanced tool tips
local enhanced = true -- comment out this line if enhanced tool tips are not required
if enhanced then
local tblB = {btnSingle, btnMultiple, btnTemplate, btnHelp, btnClose}
for _, control in ipairs(tblB) do
control.TipBalloon = 'YES'
control.TipBalloonTitleIcon = 1 -- modify individually if different
end
btnSingle.TipBalloonTitle = 'Single Source'
btnMultiple.TipBalloonTitle = 'Multiple Sources'
btnTemplate.TipBalloonTitle = 'Split by Template'
btnHelp.TipBalloonTitle = 'Help'
btnClose.TipBalloonTitle = 'Close'
end
local SplitButtons = iup.vbox{iup.fill{}, btnSingle, btnMultiple, btnTemplate, iup.fill{};
margin = '0x10', normalizesize = 'Both', padding = 10, gap = 20}
local AdminButtons = iup.hbox{iup.fill{}, btnHelp, btnClose, iup.fill{};
margin = '0x10', normalizesize = 'Both', padding = 10, gap = 50}
local vbox = iup.vbox{lblTitle, SplitButtons, AdminButtons; alignment = 'ACENTER'; margin = '20x20'}
local dialog = iup.dialog{vbox; title = 'Lumped Source Splitter (1.4.1)',
resize = 'No', minbox = 'No'}
if fhGetAppVersion() > 6 then
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
end
function btnSingle:action()
local tblS = fhPromptUserForRecordSel('SOUR', 1, dialog.HWND)
ProcessSources(tblS)
return iup.CLOSE
end
function btnMultiple:action()
local tblS = fhPromptUserForRecordSel('SOUR', -1, dialog.HWND)
ProcessSources(tblS)
return iup.CLOSE
end
function btnTemplate:action()
local tblT = fhPromptUserForRecordSel('_SRCT', 1, dialog.HWND)
SelectSources(tblT)
return iup.CLOSE
end
dialog:popup()
-- report plugin actions
if #gblReport == 0 then return end
local tblS = {}
local tblT = {}
local tblA = {}
local tblC = {}
local tblR = {}
local p = fhNewItemPtr()
for _, v in pairs(gblReport) do
p:MoveToRecordById('SOUR', v.ID)
table.insert(tblS, p:Clone())
table.insert(tblT, fhGetItemPtr(p, '~._SRCT'))
table.insert(tblA, v.All)
table.insert(tblC, v.Citations)
table.insert(tblR, fhCallBuiltInFunction('LinksTo', p))
end
fhOutputResultSetTitles('Split Sources')
fhOutputResultSetColumn('Source', 'item', tblS, #tblS, 120, 'align_left', 2)
fhOutputResultSetColumn('Template', 'item', tblT, #tblT, 120)
fhOutputResultSetColumn('All Citations', 'integer', tblA, #tblA, 60, 'align_mid')
fhOutputResultSetColumn('Lumped Citations', 'integer', tblC, #tblC, 80, 'align_mid', 1, false)
fhOutputResultSetColumn('Remaining Citations', 'integer', tblR, #tblR, 80, 'align_mid')
end
-- ************************************************************************** --
function SelectSources(tblT)
if #tblT == 0 then return end -- nothing selected
local tblS = {}
local p = fhNewItemPtr()
p:MoveToFirstRecord('SOUR')
while p:IsNotNull() do
local pL = fhGetItemPtr(p, '~._SRCT')
local pT = fhGetValueAsLink(pL)
if pT:IsSame(tblT[1]) then
table.insert(tblS, p:Clone())
end
p:MoveNext()
end
ProcessSources(tblS)
end
-- ************************************************************************** --
function ProcessSources(tblS)
local tblX = {}
local tblTitleFields = {}
if #tblS == 0 then return end -- nothing selected
local tblAllCitations = PopulateCitations() -- generate master citation list
local changed
-- now start processing selected sources
for _, pSource in ipairs(tblS) do
-- find citations to selected source
local tblCitations = GetCitations(pSource, tblAllCitations)
local CitationCount = 0
for k, _ in pairs(tblCitations) do CitationCount = CitationCount + 1 end
-- report results
local tblT = {}
tblT.ID = fhGetRecordId(pSource)
tblT.All = fhCallBuiltInFunction('LinksTo', pSource)
tblT.Citations = CitationCount
table.insert(gblReport, tblT)
-- create new sources
if CitationCount > 0 then
changed = true
local pL = fhGetItemPtr(pSource, '~._SRCT') -- check for templated source
local pT = fhNewItemPtr()
if pL:IsNotNull() then
pT = ProcessTemplate(fhGetValueAsLink(pL))
end
for _, Citations in pairs(tblCitations) do
CreateNewSource(pSource, Citations, pT)
end
fhUpdateDisplay()
-- warn if source still has citations
if fhCallBuiltInFunction('LinksTo', pSource) > 0 then
local msg = fhGetDisplayText(pSource) .. ' still has remaining citations. This is ' ..
'usually caused by citations that do not contain any detailed content.'
MessageBox(msg, 'OK', 'WARNING')
end
end
end
-- warn if no changes
if not changed then
MessageBox('No lumped citations in selection.', 'OK', 'INFORMATION')
else
MessageBox('Processing completed successfully.', 'OK', 'INFORMATION')
end
end
-- ************************************************************************** --
function PopulateCitations(ItemCount, tblAllCitations)
-- creates a master table of all source citations organised by source,
-- but not yet any analysis of those citations
local ItemCount = 0
local tblC = {}
local p = fhNewItemPtr()
p:MoveToFirstRecord('SOUR')
while p:IsNotNull() do
tblC[fhGetRecordId(p)] = {}
ItemCount = ItemCount + fhCallBuiltInFunction('LinksTo', p)
p:MoveNext()
end
-- populate source table
local Count = 0
local Now
local LastPing = os.clock()
-- display progress bar
local ProgressBar = iup.progressbar{expand = 'HORIZONTAL', max = ItemCount}
local Label = iup.label{expand = 'HORIZONTAL', alignment = 'ACENTER'}
local imgIcon = iup.animatedlabel{animation = 'IUP_CircleProgressAnimation'}
iup.SetAttribute(imgIcon, 'START', 'YES')
local vbox = iup.vbox{ProgressBar, imgIcon, Label; gap = 10, alignment = 'ACENTER', margin = '0x15'}
local dialog = iup.dialog{vbox; title = 'Counting citations...', dialogframe = 'YES', border = 'YES',
size = '300x', menubox = 'NO'}
dialog:showxy(iup.CENTER, iup.CENTER) -- Put up Progress Display
for _, RecType in ipairs({'INDI', 'FAM', 'OBJ', 'NOTE'}) do
p:MoveToFirstRecord(RecType)
while p:IsNotNull() do
if fhGetTag(fhGetValueAsLink(p)) == 'SOUR' then
table.insert(tblC[fhGetRecordId(fhGetValueAsLink(p))], p:Clone())
Count = Count + 1
end
collectgarbage('step', 0)
if Count%200 == 0 then
Label.title = 'Records checked: ' .. string.format('%.1f', Count/ItemCount*100) .. '%'
ProgressBar.value = Count
end
Now = os.clock()
if Now - LastPing > 2 then
fhExhibitResponsiveness()
LastPing = Now
end
p:MoveNextSpecial()
end
end
dialog:destroy()
return tblC
end
-- ************************************************************************** --
function ProcessTemplate(pT)
local match
local p = fhNewItemPtr()
local FieldProfile = GetFields(pT, false)
-- compare template with existing ones
p:MoveToFirstRecord('_SRCT')
while p:IsNotNull() do
if GetFields(p, true) == FieldProfile then -- templates match
return(p:Clone())
end
p:MoveNext()
end
-- no match obtained, so copy template and convert citation fields to source
local pNew = fhCreateItem('_SRCT')
CopyChildren(pT, pNew) -- Copy all details from template to new record
-- modify new copy
local TemplateName = fhGetItemText(pT, '~.NAME')
fhSetValueAsText(fhGetItemPtr(pNew, '~.NAME'), TemplateName .. ' (Split copy)')
-- create a basic title format if none exists already
local pTitle = fhGetItemPtr(pNew, '~.TITL')
local RecordTitle = ''
if pTitle:IsNotNull() then
RecordTitle = fhGetValueAsText(pTitle)
else -- automatic title not defined
pTitle = fhCreateItem('TITL', pNew)
RecordTitle = fhGetItemText(pT, '~.NAME')
p:MoveTo(pNew, '~.FDEF')
while p:IsNotNull() do
local pCit = fhGetItemPtr(p, '~.CITN')
if pCit:IsNull() then -- existing source level field
RecordTitle = RecordTitle .. ' - {' .. fhGetItemText(p, '~.CODE') .. '}'
end
p:MoveNext('SAME_TAG')
end
fhSetValueAsText(pTitle, RecordTitle)
end
-- move citation fields to source level and add to title format
local tblX = {}
p:MoveTo(pNew, '~.FDEF')
while p:IsNotNull() do
local pCit = fhGetItemPtr(p, '~.CITN')
if pCit:IsNotNull() then -- add field to auto title
RecordTitle = RecordTitle .. ' - {' .. fhGetItemText(p, '~.CODE') .. '}'
table.insert(tblX, pCit:Clone()) -- delete as no longer a citation field
end
p:MoveNext('SAME_TAG')
end
fhSetValueAsText(pTitle, RecordTitle)
for _, pX in ipairs(tblX) do fhDeleteItem(pX) end
return pNew
end
-- ************************************************************************** --
function GetFields(pT, IncludeCitation)
-- return string representing template field profile
local p = fhNewItemPtr()
local tblFields = {}
p:MoveTo(pT, '~.FDEF')
while p:IsNotNull() do
local F = fhGetItemText(p, '~.NAME') .. fhGetItemText(p, '~.TYPE')
if IncludeCitation and fhGetItemPtr(p, '~.CITN'):IsNotNull() then F = F .. 'Citation' end
table.insert(tblFields, F)
p:MoveNext('SAME_TAG')
end
table.sort(tblFields)
return table.concat(tblFields)
end
-- ************************************************************************** --
function GetCitations(pSource, tblAllCitations)
-- returns a table of citation fingerprints for all citations to the defined source
local tblCitations = {}
for _, Citation in ipairs(tblAllCitations[fhGetRecordId(pSource)]) do
local k = GetCitationFingerprint(Citation)
if k then
if not tblCitations[k] then tblCitations[k] = {} end
table.insert(tblCitations[k], Citation:Clone())
end
end
return tblCitations
end
-- ************************************************************************** --
function GetCitationFingerprint(Citation)
-- returns a text representation of the specified citation
local p = fhNewItemPtr()
local tblK = {} -- text representation of pointers for easy comparison
for _, Tag in ipairs({'~.PAGE', '~.DATA.TEXT', '~.OBJE', '~._FIELD'}) do
p:MoveTo(Citation, Tag)
while p:IsNotNull() do
table.insert(tblK, fhGetDisplayText(p))
p:MoveNext('SAME_TAG')
end
end
if #tblK > 0 then return table.concat(tblK) end
end
-- ************************************************************************** --
function CreateNewSource(pSource, Citations, pT)
local p = fhNewItemPtr()
local CommonCitation = Citations[1] -- lumped elements of citation are all the same, so use first one
-- create new Source Record as copy of original lumped source
local pNewSource = fhCreateItem('SOUR')
CopyChildren(pSource, pNewSource)
-- change template or modify generic source title
if pT:IsNotNull() then
fhSetValueAsLink(fhGetItemPtr(pNewSource, '~._SRCT'), pT)
else
local Modifier = fhGetItemText(CommonCitation, '~.PAGE')
p:MoveTo(pNewSource, '~.TITL')
if p:IsNotNull() and Modifier ~= '' then
fhSetValueAsText(p, fhGetValueAsText(p) .. ': ' .. Modifier)
end
p:MoveTo(pNewSource, '~.ABBR')
if p:IsNotNull() and Modifier ~= '' then
fhSetValueAsText(p, fhGetValueAsText(p) .. ': ' .. Modifier)
end
end
-- copy common citation data to source
p:MoveToFirstChildItem(CommonCitation)
while p:IsNotNull() do
local Tag = fhGetTag(p)
if Tag == 'PAGE' then
local pPUBL = fhCreateItem('PUBL', pNewSource)
fhSetValue_Copy(pPUBL, p)
elseif Tag == 'DATA' then
local pCitText = fhNewItemPtr()
pCitText:MoveTo(p, '~.TEXT')
if pCitText:IsNotNull() then
local pTEXT = fhCreateItem('TEXT', pNewSource)
fhSetValue_Copy(pTEXT, pCitText)
end
elseif Tag == 'OBJE' then
local pNew = fhCreateItem(Tag, pNewSource)
fhSetValue_Copy(pNew, p)
elseif Tag == '_FIELD' then
local pNew = fhCreateItem(fhGetMetafieldShortcut(p), pNewSource)
fhSetValue_Copy(pNew, p)
end
p:MoveNext()
end
fhSrcEnableAutoTitle(pNewSource, true) -- update title for templated source
-- source is now fully prepared, so create individual citations to it
local tblXcit = {} -- deletion bin for redundant citations
for _, Citation in pairs(Citations) do
local pParent = fhNewItemPtr()
pParent:MoveToParentItem(Citation)
-- create new source link
local pNew = fhCreateItem('SOUR', pParent)
fhSetValueAsLink(pNew, pNewSource)
-- delete redundant fields in lumped source citation
local tblX = {} -- deletion bin for redundant fields
local tblFields = {'~.PAGE', '~.DATA.TEXT', '~.OBJE', '_FIELD'}
for _, Field in ipairs(tblFields) do
p:MoveTo(Citation, Field)
while p:IsNotNull() do
table.insert(tblX, p:Clone())
p:MoveNext('SAME_TAG')
end
end
-- delete DATA field if no children
p:MoveTo(Citation, '~.DATA')
if not fhHasChildItem(p) then table.insert(tblX, p:Clone()) end
-- delete field pointers now as next step is dependent on this
for _, pX in ipairs(tblX) do fhDeleteItem(pX) end
-- copy any remaining lumped citation fields to new citation
p:MoveToFirstChildItem(Citation)
while p:IsNotNull() do
local Tag = fhGetTag(p)
local pNewField = fhCreateItem(Tag, pNew)
fhSetValue_Copy(pNewField, p)
p:MoveNext()
end
-- process any citation Entry Date
p:MoveTo(Citation, '~.DATA.DATE')
if p:IsNotNull() then
local pDATA = fhNewItemPtr()
pDATA:MoveTo(pNew, '~.DATA')
local pDATE = fhCreateItem('DATE', pDATA)
fhSetValue_Copy(pDATE, p)
end
-- finally, delete old lumped citation
table.insert(tblXcit, Citation:Clone())
end
for _, pX in ipairs(tblXcit) do fhDeleteItem(pX) end -- empty the citation bin
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 'Lumped Source Splitter (1.4.1)', buttondefault = Default}
-- display message box and return selection
msgdlg:popup()
return tonumber(msgdlg.ButtonResponse)
end
-- *********************************************************************
function CopyChildren(ptrSource,ptrTarget)
local ptrFrom = fhNewItemPtr()
ptrFrom = ptrSource:Clone()
ptrFrom:MoveToFirstChildItem(ptrFrom)
while ptrFrom:IsNotNull() do
CopyBranch(ptrFrom,ptrTarget)
ptrFrom:MoveNext()
end
end
-- *********************************************************************
function CopyBranch(ptrSource, ptrTarget)
local strTag = fhGetTag(ptrSource)
if strTag == '_FMT' then return end -- Skip rich text format code
if strTag == '_FIELD' then -- Substitute metafield shortcut
strTag = fhGetMetafieldShortcut(ptrSource)
end
local ptrNew = fhCreateItem(strTag, ptrTarget,true)
if ptrNew:IsNull() then return end -- Escape if item not created
fhSetValue_Copy(ptrNew, ptrSource)
CopyChildren(ptrSource, ptrNew)
end
-- *********************************************************************
main()
--[[
@Title: Lumped Source Splitter
@Type: Standard
@Author: Mark Draper
@Version: 1.5
@LastUpdated: 25 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: Converts a lumped source to individual split sources. Citations are regarded as equivalent
if they have identical Where within Source and Text from Source (or Citation level fields for Templated
Sources), and the same attached Media or Note Records. All of these items are moved to the new split
source and the original lumped citation deleted.
]]
--[[
Version 1.2 (Oct 2021)
- Initial Plugin Store version, equivalent to FHUG version 1.1.3
Version 1.3 (Jun 2023)
- Redesigned treatment of templated sources, moving citation data to source level
- Improved messaging and reporting
- Numerous improvements in coding to speed up execution time
Version 1.4 (Jul 2023)
- Citation notes now ignored for splitting
- Minor menu presentation improvements
Version 1.4.1 (Feb 2024)
- Corrects bug whereby media are retained in citation
Version 1.5 (Apr 2025)
- Support for multiple monitors added
- Support for enhanced tooltips added
]]
fhInitialise(7, 0, 15, 'save_recommended')
require('iuplua')
require('iupluaimglib')
fhu = require('fhUtils')
fhu.setIupDefaults()
iup.SetGlobal('IMAGESTOCKSIZE', 32)
-- *********************************************************************
function main()
gblReport = {} -- initialize table for plugin results
local selection
local msgTitle = 'This plugin converts a lumped source into individual split sources and recombines\n' ..
'duplicated citations created during import from another genealogy application'
local lblTitle = iup.label{title = msgTitle}
local btnSingle = iup.button{title = 'Single Source', padding = '10x3',
tip = 'Select a single source for splitting'}
local btnMultiple = iup.button{title = 'Multiple Sources',
tip = 'Select multiple sources for splitting'}
local btnTemplate = iup.button{title = 'Split by Template',
tip = 'Split all sources with same template'}
local btnHelp = iup.button{title = 'Help',
action = function(self) fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/lumped-source-splitter') end,
tip = 'Show plugin help page'}
local btnClose = iup.button{title = 'Close',
action = function(self) return iup.CLOSE end, padding = '10x3',
tip = 'Close plugin'}
-- define enhanced tool tips
local enhanced = true -- comment out this line if enhanced tool tips are not required
if enhanced then
local tblB = {btnSingle, btnMultiple, btnTemplate, btnHelp, btnClose}
for _, control in ipairs(tblB) do
control.TipBalloon = 'YES'
control.TipBalloonTitleIcon = 1 -- modify individually if different
end
btnSingle.TipBalloonTitle = 'Single Source'
btnMultiple.TipBalloonTitle = 'Multiple Sources'
btnTemplate.TipBalloonTitle = 'Split by Template'
btnHelp.TipBalloonTitle = 'Help'
btnClose.TipBalloonTitle = 'Close'
end
local SplitButtons = iup.vbox{iup.fill{}, btnSingle, btnMultiple, btnTemplate, iup.fill{};
margin = '0x10', normalizesize = 'Both', padding = 10, gap = 20}
local AdminButtons = iup.hbox{iup.fill{}, btnHelp, btnClose, iup.fill{};
margin = '0x10', normalizesize = 'Both', padding = 10, gap = 50}
local vbox = iup.vbox{lblTitle, SplitButtons, AdminButtons; alignment = 'ACENTER'; margin = '20x20'}
local dialog = iup.dialog{vbox; title = 'Lumped Source Splitter (1.4.1)',
resize = 'No', minbox = 'No'}
if fhGetAppVersion() > 6 then
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
end
function btnSingle:action()
local tblS = fhPromptUserForRecordSel('SOUR', 1, dialog.HWND)
ProcessSources(tblS)
return iup.CLOSE
end
function btnMultiple:action()
local tblS = fhPromptUserForRecordSel('SOUR', -1, dialog.HWND)
ProcessSources(tblS)
return iup.CLOSE
end
function btnTemplate:action()
local tblT = fhPromptUserForRecordSel('_SRCT', 1, dialog.HWND)
SelectSources(tblT)
return iup.CLOSE
end
dialog:popup()
-- report plugin actions
if #gblReport == 0 then return end
local tblS = {}
local tblT = {}
local tblA = {}
local tblC = {}
local tblR = {}
local p = fhNewItemPtr()
for _, v in pairs(gblReport) do
p:MoveToRecordById('SOUR', v.ID)
table.insert(tblS, p:Clone())
table.insert(tblT, fhGetItemPtr(p, '~._SRCT'))
table.insert(tblA, v.All)
table.insert(tblC, v.Citations)
table.insert(tblR, fhCallBuiltInFunction('LinksTo', p))
end
fhOutputResultSetTitles('Split Sources')
fhOutputResultSetColumn('Source', 'item', tblS, #tblS, 120, 'align_left', 2)
fhOutputResultSetColumn('Template', 'item', tblT, #tblT, 120)
fhOutputResultSetColumn('All Citations', 'integer', tblA, #tblA, 60, 'align_mid')
fhOutputResultSetColumn('Lumped Citations', 'integer', tblC, #tblC, 80, 'align_mid', 1, false)
fhOutputResultSetColumn('Remaining Citations', 'integer', tblR, #tblR, 80, 'align_mid')
end
-- ************************************************************************** --
function SelectSources(tblT)
if #tblT == 0 then return end -- nothing selected
local tblS = {}
local p = fhNewItemPtr()
p:MoveToFirstRecord('SOUR')
while p:IsNotNull() do
local pL = fhGetItemPtr(p, '~._SRCT')
local pT = fhGetValueAsLink(pL)
if pT:IsSame(tblT[1]) then
table.insert(tblS, p:Clone())
end
p:MoveNext()
end
ProcessSources(tblS)
end
-- ************************************************************************** --
function ProcessSources(tblS)
local tblX = {}
local tblTitleFields = {}
if #tblS == 0 then return end -- nothing selected
local tblAllCitations = PopulateCitations() -- generate master citation list
local changed
-- now start processing selected sources
for _, pSource in ipairs(tblS) do
-- find citations to selected source
local tblCitations = GetCitations(pSource, tblAllCitations)
local CitationCount = 0
for k, _ in pairs(tblCitations) do CitationCount = CitationCount + 1 end
-- report results
local tblT = {}
tblT.ID = fhGetRecordId(pSource)
tblT.All = fhCallBuiltInFunction('LinksTo', pSource)
tblT.Citations = CitationCount
table.insert(gblReport, tblT)
-- create new sources
if CitationCount > 0 then
changed = true
local pL = fhGetItemPtr(pSource, '~._SRCT') -- check for templated source
local pT = fhNewItemPtr()
if pL:IsNotNull() then
pT = ProcessTemplate(fhGetValueAsLink(pL))
end
for _, Citations in pairs(tblCitations) do
CreateNewSource(pSource, Citations, pT)
end
fhUpdateDisplay()
-- warn if source still has citations
if fhCallBuiltInFunction('LinksTo', pSource) > 0 then
local msg = fhGetDisplayText(pSource) .. ' still has remaining citations. This is ' ..
'usually caused by citations that do not contain any detailed content.'
MessageBox(msg, 'OK', 'WARNING')
end
end
end
-- warn if no changes
if not changed then
MessageBox('No lumped citations in selection.', 'OK', 'INFORMATION')
else
MessageBox('Processing completed successfully.', 'OK', 'INFORMATION')
end
end
-- ************************************************************************** --
function PopulateCitations(ItemCount, tblAllCitations)
-- creates a master table of all source citations organised by source,
-- but not yet any analysis of those citations
local ItemCount = 0
local tblC = {}
local p = fhNewItemPtr()
p:MoveToFirstRecord('SOUR')
while p:IsNotNull() do
tblC[fhGetRecordId(p)] = {}
ItemCount = ItemCount + fhCallBuiltInFunction('LinksTo', p)
p:MoveNext()
end
-- populate source table
local Count = 0
local Now
local LastPing = os.clock()
-- display progress bar
local ProgressBar = iup.progressbar{expand = 'HORIZONTAL', max = ItemCount}
local Label = iup.label{expand = 'HORIZONTAL', alignment = 'ACENTER'}
local imgIcon = iup.animatedlabel{animation = 'IUP_CircleProgressAnimation'}
iup.SetAttribute(imgIcon, 'START', 'YES')
local vbox = iup.vbox{ProgressBar, imgIcon, Label; gap = 10, alignment = 'ACENTER', margin = '0x15'}
local dialog = iup.dialog{vbox; title = 'Counting citations...', dialogframe = 'YES', border = 'YES',
size = '300x', menubox = 'NO'}
dialog:showxy(iup.CENTER, iup.CENTER) -- Put up Progress Display
for _, RecType in ipairs({'INDI', 'FAM', 'OBJ', 'NOTE'}) do
p:MoveToFirstRecord(RecType)
while p:IsNotNull() do
if fhGetTag(fhGetValueAsLink(p)) == 'SOUR' then
table.insert(tblC[fhGetRecordId(fhGetValueAsLink(p))], p:Clone())
Count = Count + 1
end
collectgarbage('step', 0)
if Count%200 == 0 then
Label.title = 'Records checked: ' .. string.format('%.1f', Count/ItemCount*100) .. '%'
ProgressBar.value = Count
end
Now = os.clock()
if Now - LastPing > 2 then
fhExhibitResponsiveness()
LastPing = Now
end
p:MoveNextSpecial()
end
end
dialog:destroy()
return tblC
end
-- ************************************************************************** --
function ProcessTemplate(pT)
local match
local p = fhNewItemPtr()
local FieldProfile = GetFields(pT, false)
-- compare template with existing ones
p:MoveToFirstRecord('_SRCT')
while p:IsNotNull() do
if GetFields(p, true) == FieldProfile then -- templates match
return(p:Clone())
end
p:MoveNext()
end
-- no match obtained, so copy template and convert citation fields to source
local pNew = fhCreateItem('_SRCT')
CopyChildren(pT, pNew) -- Copy all details from template to new record
-- modify new copy
local TemplateName = fhGetItemText(pT, '~.NAME')
fhSetValueAsText(fhGetItemPtr(pNew, '~.NAME'), TemplateName .. ' (Split copy)')
-- create a basic title format if none exists already
local pTitle = fhGetItemPtr(pNew, '~.TITL')
local RecordTitle = ''
if pTitle:IsNotNull() then
RecordTitle = fhGetValueAsText(pTitle)
else -- automatic title not defined
pTitle = fhCreateItem('TITL', pNew)
RecordTitle = fhGetItemText(pT, '~.NAME')
p:MoveTo(pNew, '~.FDEF')
while p:IsNotNull() do
local pCit = fhGetItemPtr(p, '~.CITN')
if pCit:IsNull() then -- existing source level field
RecordTitle = RecordTitle .. ' - {' .. fhGetItemText(p, '~.CODE') .. '}'
end
p:MoveNext('SAME_TAG')
end
fhSetValueAsText(pTitle, RecordTitle)
end
-- move citation fields to source level and add to title format
local tblX = {}
p:MoveTo(pNew, '~.FDEF')
while p:IsNotNull() do
local pCit = fhGetItemPtr(p, '~.CITN')
if pCit:IsNotNull() then -- add field to auto title
RecordTitle = RecordTitle .. ' - {' .. fhGetItemText(p, '~.CODE') .. '}'
table.insert(tblX, pCit:Clone()) -- delete as no longer a citation field
end
p:MoveNext('SAME_TAG')
end
fhSetValueAsText(pTitle, RecordTitle)
for _, pX in ipairs(tblX) do fhDeleteItem(pX) end
return pNew
end
-- ************************************************************************** --
function GetFields(pT, IncludeCitation)
-- return string representing template field profile
local p = fhNewItemPtr()
local tblFields = {}
p:MoveTo(pT, '~.FDEF')
while p:IsNotNull() do
local F = fhGetItemText(p, '~.NAME') .. fhGetItemText(p, '~.TYPE')
if IncludeCitation and fhGetItemPtr(p, '~.CITN'):IsNotNull() then F = F .. 'Citation' end
table.insert(tblFields, F)
p:MoveNext('SAME_TAG')
end
table.sort(tblFields)
return table.concat(tblFields)
end
-- ************************************************************************** --
function GetCitations(pSource, tblAllCitations)
-- returns a table of citation fingerprints for all citations to the defined source
local tblCitations = {}
for _, Citation in ipairs(tblAllCitations[fhGetRecordId(pSource)]) do
local k = GetCitationFingerprint(Citation)
if k then
if not tblCitations[k] then tblCitations[k] = {} end
table.insert(tblCitations[k], Citation:Clone())
end
end
return tblCitations
end
-- ************************************************************************** --
function GetCitationFingerprint(Citation)
-- returns a text representation of the specified citation
local p = fhNewItemPtr()
local tblK = {} -- text representation of pointers for easy comparison
for _, Tag in ipairs({'~.PAGE', '~.DATA.TEXT', '~.OBJE', '~._FIELD'}) do
p:MoveTo(Citation, Tag)
while p:IsNotNull() do
table.insert(tblK, fhGetDisplayText(p))
p:MoveNext('SAME_TAG')
end
end
if #tblK > 0 then return table.concat(tblK) end
end
-- ************************************************************************** --
function CreateNewSource(pSource, Citations, pT)
local p = fhNewItemPtr()
local CommonCitation = Citations[1] -- lumped elements of citation are all the same, so use first one
-- create new Source Record as copy of original lumped source
local pNewSource = fhCreateItem('SOUR')
CopyChildren(pSource, pNewSource)
-- change template or modify generic source title
if pT:IsNotNull() then
fhSetValueAsLink(fhGetItemPtr(pNewSource, '~._SRCT'), pT)
else
local Modifier = fhGetItemText(CommonCitation, '~.PAGE')
p:MoveTo(pNewSource, '~.TITL')
if p:IsNotNull() and Modifier ~= '' then
fhSetValueAsText(p, fhGetValueAsText(p) .. ': ' .. Modifier)
end
p:MoveTo(pNewSource, '~.ABBR')
if p:IsNotNull() and Modifier ~= '' then
fhSetValueAsText(p, fhGetValueAsText(p) .. ': ' .. Modifier)
end
end
-- copy common citation data to source
p:MoveToFirstChildItem(CommonCitation)
while p:IsNotNull() do
local Tag = fhGetTag(p)
if Tag == 'PAGE' then
local pPUBL = fhCreateItem('PUBL', pNewSource)
fhSetValue_Copy(pPUBL, p)
elseif Tag == 'DATA' then
local pCitText = fhNewItemPtr()
pCitText:MoveTo(p, '~.TEXT')
if pCitText:IsNotNull() then
local pTEXT = fhCreateItem('TEXT', pNewSource)
fhSetValue_Copy(pTEXT, pCitText)
end
elseif Tag == 'OBJE' then
local pNew = fhCreateItem(Tag, pNewSource)
fhSetValue_Copy(pNew, p)
elseif Tag == '_FIELD' then
local pNew = fhCreateItem(fhGetMetafieldShortcut(p), pNewSource)
fhSetValue_Copy(pNew, p)
end
p:MoveNext()
end
fhSrcEnableAutoTitle(pNewSource, true) -- update title for templated source
-- source is now fully prepared, so create individual citations to it
local tblXcit = {} -- deletion bin for redundant citations
for _, Citation in pairs(Citations) do
local pParent = fhNewItemPtr()
pParent:MoveToParentItem(Citation)
-- create new source link
local pNew = fhCreateItem('SOUR', pParent)
fhSetValueAsLink(pNew, pNewSource)
-- delete redundant fields in lumped source citation
local tblX = {} -- deletion bin for redundant fields
local tblFields = {'~.PAGE', '~.DATA.TEXT', '~.OBJE', '_FIELD'}
for _, Field in ipairs(tblFields) do
p:MoveTo(Citation, Field)
while p:IsNotNull() do
table.insert(tblX, p:Clone())
p:MoveNext('SAME_TAG')
end
end
-- delete DATA field if no children
p:MoveTo(Citation, '~.DATA')
if not fhHasChildItem(p) then table.insert(tblX, p:Clone()) end
-- delete field pointers now as next step is dependent on this
for _, pX in ipairs(tblX) do fhDeleteItem(pX) end
-- copy any remaining lumped citation fields to new citation
p:MoveToFirstChildItem(Citation)
while p:IsNotNull() do
local Tag = fhGetTag(p)
local pNewField = fhCreateItem(Tag, pNew)
fhSetValue_Copy(pNewField, p)
p:MoveNext()
end
-- process any citation Entry Date
p:MoveTo(Citation, '~.DATA.DATE')
if p:IsNotNull() then
local pDATA = fhNewItemPtr()
pDATA:MoveTo(pNew, '~.DATA')
local pDATE = fhCreateItem('DATE', pDATA)
fhSetValue_Copy(pDATE, p)
end
-- finally, delete old lumped citation
table.insert(tblXcit, Citation:Clone())
end
for _, pX in ipairs(tblXcit) do fhDeleteItem(pX) end -- empty the citation bin
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 'Lumped Source Splitter (1.4.1)', buttondefault = Default}
-- display message box and return selection
msgdlg:popup()
return tonumber(msgdlg.ButtonResponse)
end
-- *********************************************************************
function CopyChildren(ptrSource,ptrTarget)
local ptrFrom = fhNewItemPtr()
ptrFrom = ptrSource:Clone()
ptrFrom:MoveToFirstChildItem(ptrFrom)
while ptrFrom:IsNotNull() do
CopyBranch(ptrFrom,ptrTarget)
ptrFrom:MoveNext()
end
end
-- *********************************************************************
function CopyBranch(ptrSource, ptrTarget)
local strTag = fhGetTag(ptrSource)
if strTag == '_FMT' then return end -- Skip rich text format code
if strTag == '_FIELD' then -- Substitute metafield shortcut
strTag = fhGetMetafieldShortcut(ptrSource)
end
local ptrNew = fhCreateItem(strTag, ptrTarget,true)
if ptrNew:IsNull() then return end -- Escape if item not created
fhSetValue_Copy(ptrNew, ptrSource)
CopyChildren(ptrSource, ptrNew)
end
-- *********************************************************************
main()
Source:Lumped-Source-Splitter-6.fh_lua