Search and Return Result Set.fh_lua

--[[
@title: Search and Return Result Set
@author: Calico Pie
@lastupdated: 14 Dec 2020
@version: 1.6
@description:
Search for a specified word or phrase in all text fields in the current project, with options to narrow the scope of the search.
The matching fields are listed in a table in the Query Window result set, 
with links back to each context of use (double-click on any Item to view it in the Property Box).

1.6 Correct RichText Element support
1.5 V7 Compatibilty
1.4 Use prototype functions and fix issue with case sensitive searching.
1.3 Added Whole Word option and improve the message prompting. 
1.2 Added Case insensitive searching.
]]
-------------
-- Main Code
-------------
require "iuplua"
iup.SetGlobal("CUSTOMQUITMESSAGE","YES")


function mainfunction()
    local tblRecord = {}
    local tblItem = {}
    local tblRecordType = {}
    local tblItemDataRef = {}
    local iCount = 0
    -- Prompt for Options
    -- Prompt for confirmation using iup.GetParm %b are tick boxes
    -- set initial values
    local pa ={text=1,longtext=1,name=1,place=1,wordlist=1}
    local pConfirm = 1
    local pSensitive = 0
    local pWhole = 1
    local pPlain = 1
    local bOK,strSearch
    local ptrRecord = fhNewItemPtr()
    -- Prompt User to Confirm Options
    bOK, strSearch,pSensitive,pa.text,pa.longtext,pa.name,pa.place,pPlain,pWhole =
    iup.GetParam("Search and Return Result Set", param_action,
    "Search For: %s\n"..
    "Case Sensitive: %b\n"..
    "Search in the following Classes %t\n"..
    "Text fields: %b{Name Prefix/Suffix/Given/Nickname, Attribute values, Cause, Where Within Source, Title, Type, Custom Id, City, State, Postcode, Country, Phone, E-mail, Website, File, Format, Keywords, Sentence, etc.}\n"..
    "Long Text: %b{Note, Address, Author, Text From Source, Publication Info, Link/Note}\n"..
    "Names: %b{Name fields only}\n"..
    "Places: %b{Place fields only}\n"..
    "Extra Search Options %t\n"..
    "Plain Text Search: %b{Plain Text or Regular Expression search & replace.}\n"..
    "Whole Words (needs plain text): %b{Whole words are delimited by spaces, tabs, and punctuation.}\n"
    ,
    "",pSensitive,pa.text,pa.longtext,pa.name,pa.place,pPlain,pWhole)
    -- bOK means OK was pressed so do search
    if bOK then
        -- Set "odd" field classes as per text
        pa.word = pa.text
        pa.wordlist = pa.text
        -- Set up new search function
        local search = newSearch(strSearch,pWhole,pPlain,pSensitive)
        -- Swap 1 for true
        for strType,Value in pairs(pa) do
            pa[strType] = (Value == 1)
        end
        -- Process All Items in Data
        for ptr in allItems() do
            local strDataClass = fhGetDataClass(ptr)
			   if strDataClass == 'richtext' then strDataClass = 'text' end
			   sType = fhGetValueType(ptr)
            if (sType == 'text' or  sType == 'richtext' ) and pa[strDataClass] == true then
                local strFromString = fhGetValueAsText(ptr)
                if strDataClass == 'name' then
                    strFromString = strFromString:gsub('/',' ') -- Special Case remove // around surnames
                end
                if search(strFromString) then
                    iCount = iCount + 1
                    ptrRecord:MoveToRecordItem(ptr)
                    tblItem[iCount]= ptr:Clone()
                    tblRecord[iCount]= ptrRecord:Clone()
                    tblRecordType[iCount]= fhGetTag(ptrRecord)
                    tblItemDataRef[iCount]= BuildDR(ptr)
                end
            end
        end
        if iCount > 0 then
            local title = 'Search for '..strSearch..' in '..fhGetContextInfo('CI_PROJECT_NAME')
            fhOutputResultSetTitles(title, title, "Date: %#x")
            fhOutputResultSetColumn("Item", "item", tblItem, #tblItem, 180, "align_left")
            fhOutputResultSetColumn("Record", "item", tblRecord, #tblRecord, 180, "align_left")
            fhOutputResultSetColumn("Record Type", "text", tblRecordType, #tblRecordType, 40, "align_left")
            fhOutputResultSetColumn("Item Data Reference", "text", tblItemDataRef, #tblItemDataRef, 140, "align_left")
        else
            fhMessageBox(strSearch..' Not Found')
        end
    else
        return
    end
end

function newSearch(strSearch,pmWhole,pmPlain,pmSensitive)
    local function escape(s)
        return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'):gsub('%z','%%z'))
    end
    local function nocase (s)
        s = string.gsub(s, "%a", function (c)
            return string.format("[%s%s]", string.lower(c),
            string.upper(c))
        end)
        return s
    end
    local iPos,iPos2,iPos3
    local strSearchFor = strSearch
    local strSearchForEnd, strSearchForStart
    if pmPlain == 1 then
        strSearchFor = escape(strSearchFor)
    end
    if pmSensitive ~= 1 then
        strSearchFor = nocase(strSearchFor)
    end
    if pmWhole == 1 and pmPlain == 1 then
        strSearchForEnd = '[%s%p]'..strSearchFor..'$' -- End of Line
        strSearchForStart = '^'..strSearchFor..'[%s%p]' -- Start of Line
        strSearchFor = '[%s%p]'..strSearchFor..'[%s%p]' -- Delimited by Space or Punctuation
    end
    --- Return Function for use in data traversal.
    return

    function (strFromString)
        iPos = string.find(strFromString,strSearchFor)
        if pmWhole == 1 and pmPlain == 1 then
            -- Special Case search for last word and first word
            iPos2 = string.find(strFromString,strSearchForEnd)
            iPos3 = string.find(strFromString,strSearchForStart)
        end
        return (iPos or iPos2 or iPos3)
    end
end

function BuildDR(ptr)
    local ptrTemp = fhNewItemPtr()
    ptrTemp:MoveTo(ptr)
    strDR = fhGetTag(ptrTemp)
    while fhHasParentItem(ptrTemp) do
        ptrTemp:MoveToParentItem(ptrTemp)
        strDR = fhGetTag(ptrTemp)..'.'..strDR
    end
    return strDR
end

function allItems(...)
    
    arg = {...}
    arg['n'] = #arg

    local iTypeCount = nil
    local iPos = 1
    local p1 = fhNewItemPtr()
    local p2 = fhNewItemPtr()
    local tblRecTypes = {}
    
    if arg['n'] == 0 then
        -- No parameter do all Record Types
        iTypeCount = fhGetRecordTypeCount() -- Get Count of Record types
        for i = 1,iTypeCount do
            tblRecTypes[i] = fhGetRecordTypeTag(i)
        end
    else
        -- Got Parameters Process them instead
        tblRecTypes = arg
        iTypeCount = arg['n']
    end
    p1:MoveToFirstRecord(tblRecTypes[iPos])
    return function()
        repeat
        while p1:IsNotNull() do
            p2:MoveTo(p1)
            p1:MoveNextSpecial()
            if p2:IsNotNull() then
                return p2
            end
        end
        -- Loop through Record Types
        iPos = iPos + 1
        if iPos <= iTypeCount then
            p1:MoveToFirstRecord(tblRecTypes[iPos])
        end
        until iPos > iTypeCount
    end
end
-- Call Main
mainfunction()

Source:Search-and-Return-Result-Set-1.fh_lua