Clean Up Notes.fh_lua

--[[
@title: Clean Up Notes
@author: Jane Taubman
@lastupdated: 5 Feb 2021
@version: 1.3
@description:
Optionally
1. Removes Blank lines from the start of Notes
2. Converts Note Records with a Single Link to a Local Note.
Updates
1.1 Updated Progress Bar to latest version.
1.2 V7 Compatibility
1.3 Correct problems with Rich Text Notes with source citations in V7.
]]
require("iuplua");
iup.SetGlobal("CUSTOMQUITMESSAGE","YES");

---------------------------------------
-- Main Logic
---------------------------------------
function main()

    -- Prompt for confirmation using iup.GetParm %b are tick boxes
    -- set initial values
    premoveblanks = 1
    pconvertnotes = 1
    -- Prompt User to Confirm Options
    ret, premoveblanks, pconvertnotes =
    iup.GetParam("Family Historian Note Tidy", param_action,
    "Remove Blank Start Lines: %b \n"..
    "Convert single use Notes to Local: %b \n",
    premoveblanks, pconvertnotes)
    if (ret == false) then
        return
    end
    -- Create Pointers for Main Routine
    ptrNote = fhNewItemPtr()
    ptrNoteText = fhNewItemPtr()
    ptrTemp = fhNewItemPtr()
    ptrParent = fhNewItemPtr()
    ptrLink = fhNewItemPtr()
    ProgressDisplay.Start('Cleaning Notes')
    ProgressDisplay.SetMessage('Building Cross Reference Table . . ')
    istep = 1 / CountNotes() -- Get the step size for the Bar
    tblList = BuildNoteList()
    gaugeProgress.value = gaugeProgress.value + istep
    -- Process All Notes
    ptrNote:MoveToFirstRecord('NOTE')
    gaugeProgress.value = 0
    -- Do All Local Notes
    if premoveblanks == 1 then
        CleanLocalNotes()
    end
    ProgressDisplay.SetMessage('Processing Note Records')
    -- Process All Note Records
    while ptrNote:IsNotNull() do
        ProgressDisplay.Step(istep)
        -- allow the dialog to process any messages
        iup.LoopStep()
        -- notice the user wanting to cancel and throw error so FH prompts to undo changes
        if ProgressDisplay.Cancel() then
            error('User Cancelled')
        end
        if premoveblanks == 1 then
            -- Remove Blank Lines from Start
            ptrTemp:MoveTo(ptrNote,'~.TEXT') -- Get Text for Note
            if ptrTemp:IsNotNull() then                
                local type = fhGetValueType(ptrTemp)
                if type == 'richtext' then
                    local rt = fhGetValueAsRichText(ptrTemp)
                    local sReplace = RemoveLeadingBlanklines(rt:GetText())
                    if sReplace ~= rt:GetText() then
                        rt:SetText(sReplace,true)
                        fhSetValueAsRichText(ptrNote2,rt)
                    end
                else
                    fhSetValueAsText(ptrTemp,RemoveLeadingBlanklines(fhGetValueAsText(ptrTemp)))
                end
            end
        end
        fhUpdateDisplay()
        if pconvertnotes == 1 then
            ptrTemp:SetNull() -- Clear delete pointer
            iLinkCount = fhCallBuiltInFunction('LinksTo',ptrNote)
            if iLinkCount == 1 then -- Found a note with only one link.
                -- Convert Linked Note and flag Note Record for delete.
                ptrLink = tblList[fhGetRecordId(ptrNote)][1]
                
                if ptrLink:IsNotNull() then
                    ptrParent:MoveToParentItem(ptrLink) -- Get Notes Parent
                    ptrNote2 = fhCreateItem('NOTE2', ptrParent) -- Create NOTE2 for Parent (Local Note)
                    ptrTemp:MoveTo(ptrNote,'~.TEXT') -- Get Text for Note
                    local type = fhGetValueType(ptrTemp)
                    if type == 'richtext' then
                        local rt = fhGetValueAsRichText(ptrTemp)
                        fhSetValueAsRichText(ptrNote2,rt)
                    else
                        fhSetValueAsText(ptrNote2,fhGetValueAsText(ptrTemp))
                    end
                    
                    
                    CopyChildren(ptrNote, ptrNote2) -- Copy all the child items from the NOTE
                    CopyChildren(ptrTemp, ptrNote2) -- Copy all the child items from the NOTE
                    ptrTemp = ptrNote:Clone() -- Hold on to Note Record so it can be deleted.
                end
            end
        end
        ptrNote:MoveNext() -- Next Note
        if ptrTemp:IsNotNull() then -- Delete Original Note
            fhDeleteItem(ptrTemp)
        end
    end
    ProgressDisplay.Close() -- Clean up Progress Bar
    fhMessageBox('Note Replacement Complete','MB_OK','MB_ICONINFORMATION')
end
----------------------------------------------------------- Start Progress Display
--[[
@Title: Progress Display (drop in)
@Author: Jane Taubman
@LastUpdated: July 2011
@Description: Allows easy adding of a Progress Bar to any long running Plugin
]]

ProgressDisplay = {
    
    Start = function(strTitle,intMax) -- Create and start the Progress Display window controls
        cancelflag = false
        local cancelbutton = iup.button{ title="Cancel", size="50x20",
            action = function()
                cancelflag = true -- Signal that Cancel button was pressed
                return iup.CLOSE
            end
        }
        gaugeProgress = iup.progressbar{ expand="YES", max=intMax } -- Set progress bar maximum range
        messageline = iup.label{ title=" ", expand="YES", alignment="ACENTER" }
        dlgProgress = iup.dialog{ title=strTitle, size="QUARTER", dialogframe="YES", -- Remove Windows minimize/maximize menu
            iup.vbox{ alignment="ACENTER", gap="10",
                messageline,
                gaugeProgress,
                cancelbutton
            }
        }
        dlgProgress.close_cb = cancelbutton.action -- Windows Close button acts as Cancel button
        dlgProgress:showxy(iup.CENTER, iup.CENTER) -- Show the Progress Display dialogue window
    end,
    
    SetMessage = function(strMessage) -- Set the progress message
        messageline.title = strMessage
    end,
    
    Step = function(iStep) -- Step the Progress Bar forward
        gaugeProgress.value = gaugeProgress.value + iStep
        local val = tonumber(gaugeProgress.value)
        local max = tonumber(gaugeProgress.max)
        if val > max then
            gaugeProgress.value = 0
        end
        iup.LoopStep()
    end,
    
    Reset = function() -- Reset progress bar
        gaugeProgress.value = 0
    end,
    
    Cancel = function() -- Check if Cancel button pressed
        return cancelflag
    end,
    
    Close = function() -- Close the dialogue window
        dlgProgress:destroy()
    end
    
}
----------------------------------------------------------- End Progress Display
---------------------------
-- Count Notes (for gaugeProgress)
---------------------------

function CountNotes()
    local ptrCount = fhNewItemPtr()
    local count = 0
    ptrCount:MoveToFirstRecord('NOTE')
    while ptrCount:IsNotNull() do
        count = count + 1
        ptrCount:MoveNext()
    end
    -- Return as reciprical to step the gaugeProgress
    return count
end
---------------------------
-- BuildNoteList
---------------------------

function BuildNoteList()
    tblLinks = {}
    iCount = fhGetRecordTypeCount() -- Get Count of Record types
    local ii = 0
    local ptrLLink = fhNewItemPtr()
    local ptrLCheck = fhNewItemPtr()
    for ii =1,iCount do
        strType = fhGetRecordTypeTag(ii)
        ptrLLink:MoveToFirstRecord(strType)
        while ptrLLink:IsNotNull() do
            if fhGetDataClass(ptrLLink) == 'link' then
                ptrLCheck = fhGetValueAsLink(ptrLLink)
                if fhGetTag(ptrLCheck) == 'NOTE' then
                    recordid = fhGetRecordId(ptrLCheck)
                    if tblLinks[recordid] == nil then
                        tblLinks[recordid] = {}
                    end
                    table.insert(tblLinks[recordid],ptrLLink:Clone())
                end
            end
            ptrLLink:MoveNextSpecial()
        end
    end
    return tblLinks
end
---------------------------
-- Function: Clean Local Notes
---------------------------

function CleanLocalNotes ()
    local iCount = fhGetRecordTypeCount() -- Get Count of Record types
    -- Loop through Record Types, looking for link
    -- There can be only one.
    local ii = 0
    local ptrLLink = fhNewItemPtr()
    local ptrLCheck = fhNewItemPtr()
    ProgressDisplay.SetMessage('Cleaning Local Notes')
    ProgressDisplay.Reset()
    local iStep = 1 / iCount
    for ii =1,iCount do
        strType = fhGetRecordTypeTag(ii)
        ptrLLink:MoveToFirstRecord(strType)
        while ptrLLink:IsNotNull() do
            if fhGetTag(ptrLLink) == 'NOTE2' then
                -- If Rich process differently
                local type = fhGetValueType(ptrLLink)
                if type == 'richtext' then
                    local rt = fhGetValueAsRichText(ptrLLink)
                    local sReplace = RemoveLeadingBlanklines(rt:GetText())
                    if sReplace ~= rt:GetText() then
                        rt:SetText(sReplace,true)
                        fhSetValueAsRichText(ptrLLink,rt)
                    end
                else
                    fhSetValueAsText(ptrLLink,RemoveLeadingBlanklines(fhGetValueAsText(ptrLLink)))
                end
                ProgressDisplay.Step(0)
            end
            
            ptrLLink:MoveNextSpecial()
        end
        -- notice the user wanting to cancel and do something meaningful
        if ProgressDisplay.Cancel() then
            error('User Cancelled')
        end
        ProgressDisplay.Step(iStep)
    end
    return false, nil
end
-- Copy Notes Childen

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

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 CopyChildren
---------------------------------------
-- Function: RemoveLeadingBlanklines
---------------------------------------

function RemoveLeadingBlanklines (str)
    -- Strip all Blank Lines from the Start of the String
    while string.find(str,'^\n') == 1 do
        str = str:gsub('^\n','')
    end
    return str
end
main()

Source:Clean-Up-Notes-2.fh_lua