Quantcast

-updated LibItemSearch-1.0 to newest version

Xruptor [11-24-11 - 15:03]
-updated LibItemSearch-1.0 to newest version
Filename
libs/LibItemSearch-1.0.lua
diff --git a/libs/LibItemSearch-1.0.lua b/libs/LibItemSearch-1.0.lua
index e7f52a6..4bfdcff 100644
--- a/libs/LibItemSearch-1.0.lua
+++ b/libs/LibItemSearch-1.0.lua
@@ -1,7 +1,7 @@
 --[[
 	ItemSearch
 		An item text search engine of some sort
-
+
 	Grammar:
 		<search> 			:=	<intersect search>
 		<intersect search> 	:=	<union search> & <union search> ; <union search>
@@ -13,19 +13,60 @@
 		<ilvl search>		:=	ilvl<op><number>
 		<type search>		:=	t:<text>
 		<text search>		:=	<text>
+		<item set search>	:=	s:<setname> (setname can be * for all sets)
 		<op>				:=  : | = | == | != | ~= | < | > | <= | >=
-
-	I kindof half want to make a full parser for this
 --]]

-local MAJOR, MINOR = "LibItemSearch-1.0", 2
-local ItemSearch = LibStub:NewLibrary(MAJOR, MINOR)
-if not ItemSearch then return end
+local Lib = LibStub:NewLibrary('LibItemSearch-1.0', 7)
+if not Lib then
+  return
+else
+  Lib.searchTypes = Lib.searchTypes or {}
+end
+
+
+--[[ Locals ]]--
+
+local tonumber, select, split = tonumber, select, strsplit
+local function useful(a) -- check if the search has a decent size
+  return a and #a >= 1
+end
+
+local function compare(op, a, b)
+  if op == '<=' then
+    return a <= b
+  end
+
+  if op == '<' then
+    return a < b
+  end
+
+  if op == '>' then
+    return a > b
+  end
+
+  if op == '>=' then
+    return a >= b
+  end
+
+  return a == b
+end
+
+local function match(search, ...)
+  for i = 1, select('#', ...) do
+    local text = select(i, ...)
+    if text and text:lower():find(search) then
+      return true
+    end
+  end
+  return false
+end
+

---[[ general search ]]--
+--[[ User API ]]--

-function ItemSearch:Find(itemLink, search)
-	if not search then
+function Lib:Find(itemLink, search)
+	if not useful(search) then
 		return true
 	end

@@ -33,245 +74,212 @@ function ItemSearch:Find(itemLink, search)
 		return false
 	end

-	local search = search:lower()
-	if search:match('\124') then
-		return self:FindUnionSearch(itemLink, strsplit('\124', search))
-	end
-	return self:FindUnionSearch(itemLink, search)
+  return self:FindUnionSearch(itemLink, split('\124', search:lower()))
 end


---[[ union search: <search>&<search> ]]--
+--[[ Top-Layer Processing ]]--

-function ItemSearch:FindUnionSearch(itemLink, ...)
+-- union search: <search>&<search>
+function Lib:FindUnionSearch(item, ...)
 	for i = 1, select('#', ...) do
 		local search = select(i, ...)
-		if search and search ~= '' then
-			if search:match('\038') then
-				if self:FindIntersectSearch(itemLink, strsplit('\038', search)) then
-					return true
-				end
-			else
-				if self:FindIntersectSearch(itemLink, search) then
-					return true
-				end
-			end
+		if useful(search) and self:FindIntersectSearch(item, split('\038', search)) then
+      		return true
 		end
 	end
-	return false
 end


---[[ intersect search: <search>|<search> ]]--
-
-function ItemSearch:FindIntersectSearch(itemLink, ...)
+-- intersect search: <search>|<search>
+function Lib:FindIntersectSearch(item, ...)
 	for i = 1, select('#', ...) do
 		local search = select(i, ...)
-		if search and search ~= '' then
-			if not self:FindNegatableSearch(itemLink, search) then
-				return false
-			end
+		if useful(search) and not self:FindNegatableSearch(item, search) then
+        	return false
 		end
 	end
 	return true
 end


---[[ negated search: !<search> ]]--
-
-function ItemSearch:FindNegatableSearch(itemLink, search)
-	local negatedSearch = search:match('^\033(.+)$')
-	if negatedSearch then
-		return not self:FindTypedSearch(itemLink, negatedSearch)
-	end
-	return self:FindTypedSearch(itemLink, search)
+-- negated search: !<search>
+function Lib:FindNegatableSearch(item, search)
+  local negatedSearch = search:match('^[!~][%s]*(.+)$')
+  if negatedSearch then
+    return not self:FindTypedSearch(item, negatedSearch)
+  end
+  return self:FindTypedSearch(item, search, true)
 end


 --[[
-	typed search:
-		user defined search types
+     Search Types:
+      easly defined search types

-	A typed search object should look like the following:
-		{
-			string id
-				unique identifier for the search type,
+      A typed search object should look like the following:
+        {
+          string id
+            unique identifier for the search type,

-			string searchCapture = function isSearch(self, search)
-				returns a capture if the given search matches this typed search
-				returns nil if the search is not a match for this type
+          string searchCapture = function canSearch(self, search)
+            returns a capture if the given search matches this typed search

-			bool isMatch = function findItem(self, itemLink, searchCapture)
-				returns true if <itemLink> is in the search defined by <searchCapture>
-		}
+          bool isMatch = function findItem(self, itemLink, searchCapture)
+            returns true if <itemLink> is in the search defined by <searchCapture>
+          }
 --]]

-local typedSearches = {}
-function ItemSearch:RegisterTypedSearch(typedSearchObj)
-	typedSearches[typedSearchObj.id] = typedSearchObj
+function Lib:RegisterTypedSearch(object)
+	self.searchTypes[object.id] = object
 end

-function ItemSearch:GetTypedSearches()
-	return pairs(typedSearches)
+function Lib:GetTypedSearches()
+	return pairs(self.searchTypes)
 end

-function ItemSearch:GetTypedSearch(id)
-	return typedSearches[id]
+function Lib:GetTypedSearch(id)
+	return self.searchTypes[id]
 end

-function ItemSearch:FindTypedSearch(itemLink, search)
-	if not search then
-		return false
-	end
-
-	for id, searchInfo in self:GetTypedSearches() do
-		local capture1, capture2, capture3 = searchInfo:isSearch(search)
-		if capture1 then
-			return searchInfo:findItem(itemLink, capture1, capture2, capture3)
-		end
-	end
-
-	return self:GetTypedSearch('itemTypeGeneric'):findItem(itemLink, search) or self:GetTypedSearch('itemName'):findItem(itemLink, search)
+function Lib:FindTypedSearch(item, search, default)
+  if not useful(search) then
+    return default
+  end
+
+  local tag, rest = search:match('^[%s]*(%w+):(.*)$')
+  if tag then
+    if useful(rest) then
+      search = rest
+    else
+      return default
+    end
+  end
+
+  local operator, search = search:match('^[%s]*([%>%<%=]*)[%s]*(.*)$')
+  if useful(search) then
+    operator = useful(operator) and operator
+  else
+    return default
+  end
+
+  if tag then
+    tag = '^' .. tag
+    for id, searchType in self:GetTypedSearches() do
+      if searchType.tags then
+        for _, value in pairs(searchType.tags) do
+          if value:find(tag) then
+            return self:UseTypedSearch(searchType, item, operator, search)
+          end
+        end
+      end
+    end
+  else
+    for id, searchType in self:GetTypedSearches() do
+      if self:UseTypedSearch(searchType, item, operator, search) then
+        return true
+      end
+    end
+    return false
+  end
+
+  return default
 end

-
---[[
-	Basic typed searches
---]]
-
-function ItemSearch:Compare(op, lhs, rhs)
-	--ugly, but it works
-	if op == ':' or op == '=' or op == '==' then
-		return lhs == rhs
-	end
-	if op == '!=' or op == '~=' then
-		return lhs ~= rhs
-	end
-	if op == '<=' then
-		return lhs <= rhs
-	end
-	if op == '<' then
-		return lhs < rhs
-	end
-	if op == '>' then
-		return lhs > rhs
-	end
-	if op == '>=' then
-		return lhs >= rhs
-	end
-	return false
+function Lib:UseTypedSearch(searchType, item, operator, search)
+  local capture1, capture2, capture3 = searchType:canSearch(operator, search)
+  if capture1 then
+    if searchType:findItem(item, operator, capture1, capture2, capture3) then
+      return true
+    end
+  end
 end


---[[ basic text search n:(.+) ]]--
+--[[ Item name ]]--

-local function search_IsInText(search, ...)
-	for i = 1, select('#', ...) do
-		local text = select(i, ...)
-		text = text and tostring(text):lower()
-		if text and (text == search or text:match(search)) then
-			return true
-		end
-	end
-	return false
-end
+Lib:RegisterTypedSearch{
+  id = 'itemName',
+  tags = {'name'},

-ItemSearch:RegisterTypedSearch{
-	id = 'itemName',
-
-	isSearch = function(self, search)
-		return search and search:match('^n:(.+)$')
+	canSearch = function(self, operator, search)
+		return not operator and search
 	end,

-	findItem = function(self, itemLink, search)
-		local itemName = (GetItemInfo(itemLink))
-		return search_IsInText(search, itemName)
+	findItem = function(self, item, _, search)
+		local name = GetItemInfo(item)
+		return match(search, name)
 	end
 }


---[[ item type,subtype,equip loc search t:(.+) ]]--
+--[[ Item type, subtype and equiploc ]]--

-ItemSearch:RegisterTypedSearch{
-	id = 'itemTypeGeneric',
+Lib:RegisterTypedSearch{
+	id = 'itemType',
+	tags = {'type', 'slot'},

-	isSearch = function(self, search)
-		return search and search:match('^t:(.+)$')
+	canSearch = function(self, operator, search)
+		return not operator and search
 	end,

-	findItem = function(self, itemLink, search)
-		local name, link, quality, iLevel, reqLevel, type, subType, maxStack, equipSlot = GetItemInfo(itemLink)
-		if not name then
-			return false
-		end
-		return search_IsInText(search, type, subType, _G[equipSlot])
+	findItem = function(self, item, _, search)
+		local type, subType, _, equipSlot = select(6, GetItemInfo(item))
+		return match(search, type, subType, _G[equipSlot])
 	end
 }


---[[ item quality search: q(sign)(%d+) | q:(qualityName) ]]--
-
-ItemSearch:RegisterTypedSearch{
-	id = 'itemQuality',
-
-	isSearch = function(self, search)
-		if search then
-			return search:match('^q([%~%:%<%>%=%!]+)(%w+)$')
-		end
-	end,
+--[[ Item quality ]]--

-	descToQuality = function(self, desc)
-		local q = 0
+local qualities = {}
+for i = 0, #ITEM_QUALITY_COLORS do
+  qualities[i] = _G['ITEM_QUALITY' .. i .. '_DESC']:lower()
+end

-		local quality = _G['ITEM_QUALITY' .. q .. '_DESC']
-		while quality and quality:lower() ~= desc do
-			q = q + 1
-			quality = _G['ITEM_QUALITY' .. q .. '_DESC']
-		end
+Lib:RegisterTypedSearch{
+	id = 'itemQuality',
+	tags = {'quality'},

-		if quality then
-			return q
+	canSearch = function(self, _, search)
+		for i, name in pairs(qualities) do
+		  if name:find(search) then
+			return i
+		  end
 		end
 	end,

-	findItem = function(self, itemLink, op, search)
-		local name, link, quality = GetItemInfo(itemLink)
-		if not name then
-			return false
-		end
-
-		local num = tonumber(search) or self:descToQuality(search)
-		return num and ItemSearch:Compare(op, quality, num) or false
+	findItem = function(self, link, operator, num)
+		local quality = select(3, GetItemInfo(link))
+		return compare(operator, quality, num)
 	end,
 }

---[[ item level search: lvl(sign)(%d+) ]]--

-ItemSearch:RegisterTypedSearch{
+--[[ Item level ]]--
+
+Lib:RegisterTypedSearch{
 	id = 'itemLevel',
+	tags = {'level', 'lvl'},

-	isSearch = function(self, search)
-		if search then
-			return search:match('^ilvl([:<>=!]+)(%d+)$')
-		end
+	canSearch = function(self, _, search)
+		return tonumber(search)
 	end,

-	findItem = function(self, itemLink, op, search)
-		local name, link, quality, iLvl = GetItemInfo(itemLink)
-		if not iLvl then
-			return false
+	findItem = function(self, link, operator, num)
+		local lvl = select(4, GetItemInfo(link))
+		if lvl then
+			return compare(operator, lvl, num)
 		end
-
-		local num = tonumber(search)
-		return num and ItemSearch:Compare(op, iLvl, num) or false
 	end,
 }


---[[ tooltip keyword search ]]--
+--[[ Tooltip searches ]]--

 local tooltipCache = setmetatable({}, {__index = function(t, k) local v = {} t[k] = v return v end})
 local tooltipScanner = _G['LibItemSearchTooltipScanner'] or CreateFrame('GameTooltip', 'LibItemSearchTooltipScanner', UIParent, 'GameTooltipTemplate')
+tooltipScanner:SetOwner(UIParent, 'ANCHOR_NONE')

 local function link_FindSearchInTooltip(itemLink, search)
 	--look in the cache for the result
@@ -282,7 +290,6 @@ local function link_FindSearchInTooltip(itemLink, search)
 	end

 	--no match?, pull in the resut from tooltip parsing
-	tooltipScanner:SetOwner(UIParent, 'ANCHOR_NONE')
 	tooltipScanner:SetHyperlink(itemLink)

 	local result = false
@@ -291,127 +298,179 @@ local function link_FindSearchInTooltip(itemLink, search)
 	elseif tooltipScanner:NumLines() > 2 and _G[tooltipScanner:GetName() .. 'TextLeft3']:GetText() == search then
 		result = true
 	end
-	tooltipScanner:Hide()

 	tooltipCache[search][itemID] = result
 	return result
 end

-ItemSearch:RegisterTypedSearch{
-	id = 'tooltip',

-	isSearch = function(self, search)
+Lib:RegisterTypedSearch{
+	id = 'bindType',
+
+	canSearch = function(self, _, search)
 		return self.keywords[search]
 	end,

-	findItem = function(self, itemLink, search)
+	findItem = function(self, itemLink, _, search)
 		return search and link_FindSearchInTooltip(itemLink, search)
 	end,

 	keywords = {
+    	['soulbound'] = ITEM_BIND_ON_PICKUP,
+    	['bound'] = ITEM_BIND_ON_PICKUP,
 		['boe'] = ITEM_BIND_ON_EQUIP,
 		['bop'] = ITEM_BIND_ON_PICKUP,
 		['bou'] = ITEM_BIND_ON_USE,
 		['quest'] = ITEM_BIND_QUEST,
-		['boa'] = ITEM_BIND_TO_ACCOUNT
+		['boa'] = ITEM_BIND_TO_BNETACCOUNT
 	}
 }

+Lib:RegisterTypedSearch{
+	id = 'tooltip',
+
+	canSearch = function(self, _, search)
+		return search and search:match('^tt:(.+)$')
+	end,

---[[ equipment set search ]]--
+	findItem = function(self, itemLink, _, search)
+		tooltipScanner:SetHyperlink(itemLink)

-local function IsWardrobeLoaded()
-	local name, title, notes, enabled, loadable, reason, security = GetAddOnInfo('Wardrobe')
-	return enabled
-end
+		local i = 1
+		while i <= tooltipScanner:NumLines() do
+			local text =  _G[tooltipScanner:GetName() .. 'TextLeft' .. i]:GetText():lower()
+			if text:find(search) then
+				return true
+			end
+			i = i + 1
+		end

-local function findEquipmentSetByName(search)
-	local startsWithSearch = '^' .. search
-	local partialMatch = nil
+		return false
+	end,
+}

-	for i = 1, GetNumEquipmentSets() do
-		local setName = (GetEquipmentSetInfo(i))
-		local lSetName = setName:lower()

-		if lSetName == search then
-			return setName
-		end
+--[[ Equipment sets ]]--
+
+--Placeholder variables; will be replaced with references to the addon-appropriate handlers at runtime
+local ES_FindSets, ES_CheckItem
+
+--Helper: Global Pattern Matching Function (matches ANY set name if search is *, or the EXACT set name if exactMatch is true, or any set name STARTING with the provided search terms if exactMatch is false (this means it will not match in middle of strings). all equipment set searches below use this function to FIRST try to find a set with the EXACT name entered, and if that fails they'll look for all sets that START with the search term, using recursive calls.
+local function ES_TrySetName(setName, search, exactMatch)
+	return (search == '*') or (exactMatch and setName:lower() == search) or (not exactMatch and setName:lower():sub(1,strlen(search)) == search)
+end

-		if lSetName:match(startsWithSearch) then
-			partialMatch = setName
+--Addon Support: ItemRack
+if IsAddOnLoaded('ItemRack') then
+	function ES_FindSets(setList, search, exactMatch)
+		for setName, _ in pairs(ItemRackUser.Sets) do
+			if ES_TrySetName(setName, search, exactMatch) then
+				if (search ~= '*') or (search == '*' and setName:sub(1,1) ~= '~') then --note: this additional tilde check skips internal ItemRack sets when doing a global set search (internal sets are prefixed with tilde, such as ~Unequip, and they contain temporary data that should not be part of a global search)
+					table.insert(setList, setName)
+				end
+			end
+		end
+		if (search ~= '*') and exactMatch and #setList == 0 then --if we just finished an exact, non-global (not "*"), name match search and still have no results, try one more time with partial ("starts with") set name matching instead
+			ES_FindSets(setList, search, false)
 		end
 	end

-	-- Wardrobe Support
-	if Wardrobe then
-		for i, outfit in ipairs( Wardrobe.CurrentConfig.Outfit) do
-			local setName = outfit.OutfitName
-			local lSetName = setName:lower()
+	local irSameID = (ItemRack and ItemRack.SameID or nil) --set up local reference for speed if they're an ItemRack user
+	function ES_CheckItem(itemLink, setList)
+		local itemID = string.match(itemLink or '','item:(%-?%d+)') or 0 --grab the baseID of the item we are searching for (we don't need the full itemString, since we'll only be doing a loose baseID comparison below)
+
+		for _, setName in pairs(setList) do
+			for _, irItemData in pairs(ItemRackUser.Sets[setName].equip) do --note: do not change this to ipairs() or it will abort scanning at empty slots in a set
+				--[[ commented out due to libItemSearch lacking a "best match before generic match" priority matching system, so we'll have to go for "generic match" only (below), which matches items that have the same base ItemID as items from the set, as ItemRack cannot guarantee that the stored ItemString will be valid anymore (if the user has modified the item since last saving the set)
+				if itemString == irItemData then -- strict match: perform a strict match to check if this is the *exact* same item (same gems, enchants, etc)
+					return true
+				end]]--

-			if lSetName == search then
-				return setName
+				if irSameID(itemID, irItemData) then --loose match: use ItemRack's built-in "Base ItemID" comparison function to allow us to match any items that have the same base itemID (disregarding strict matching of gems, enchants, etc); due to libItemSearch limitations it's the best compromise and guarantees to always highlight the correct items even if we may catch some extras/duplicates that weren't part of the set
+					return true
+				end
 			end
+		end

-			if lSetName:match(startsWithSearch) then
-				partialMatch = setName
+		return false
+	end
+
+--Addon Support: Wardrobe
+elseif IsAddOnLoaded('Wardrobe') then
+	function ES_FindSets(setList, search, exactMatch)
+		for _, waOutfit in ipairs(Wardrobe.CurrentConfig.Outfit) do
+			if ES_TrySetName(waOutfit.OutfitName, search, exactMatch) then
+				table.insert(setList, waOutfit) --insert an actual reference to the matching set's data table, instead of just storing the /name/ of the set. we do this due to how Wardrobe works (all sets are in a numerically indexed table and storing the table offset would therefore be unreliable)
 			end
 		end
+		if (search ~= '*') and exactMatch and #setList == 0 then --if we just finished an exact, non-global (not "*"), name match search and still have no results, try one more time with partial ("starts with") set name matching instead
+			ES_FindSets(setList, search, false)
+		end
 	end

-	return partialMatch
-end
+	function ES_CheckItem(itemLink, setList)
+		local itemID = tonumber(string.match(itemLink or '','item:(%-?%d+)') or 0) --grab the baseID of the item we are searching for (we don't need the full itemString, since we'll only be doing a loose baseID comparison below)

-local function isItemInEquipmentSet(itemLink, setName)
-	if not setName then
-		return false
-	end
+		for _, waOutfit in pairs(setList) do
+			for _, waItemData in pairs(waOutfit.Item) do
+				if (waItemData.IsSlotUsed == 1) and (waItemData.ItemID == itemID) then --loose match: compare the current item's baseID to the baseID of the set item
+					return true
+				end
+			end
+		end

-	local itemIDs = GetEquipmentSetItemIDs(setName)
-	if not itemIDs then
 		return false
 	end

-	local itemID = tonumber(itemLink:match('item:(%d+)'))
-	for inventoryID, setItemID in pairs(itemIDs) do
-		if itemID == setItemID then
-			return true
+--Last Resort: Blizzard Equipment Manager
+else
+	function ES_FindSets(setList, search, exactMatch)
+		for i = 1, GetNumEquipmentSets() do
+			local setName = GetEquipmentSetInfo(i)
+			if ES_TrySetName(setName, search, exactMatch) then
+				table.insert(setList, setName)
+			end
+		end
+		if (search ~= '*') and exactMatch and #setList == 0 then --if we just finished an exact, non-global (not "*"), name match search and still have no results, try one more time with partial ("starts with") set name matching instead
+			ES_FindSets(setList, search, false)
 		end
 	end

-	return false
-end
+	function ES_CheckItem(itemLink, setList)
+		local itemID = tonumber(string.match(itemLink or '','item:(%-?%d+)') or 0) --grab the baseID of the item we are searching for (we don't need the full itemString, since we'll only be doing a loose baseID comparison below)

-local function isItemInWardrobeSet(itemLink, setName)
-	if not Wardrobe then return false end
-
-	local itemName = (GetItemInfo(itemLink))
-	for i, outfit in ipairs(Wardrobe.CurrentConfig.Outfit) do
-		if outfit.OutfitName == setName then
-			for j, item in pairs(outfit.Item) do
-				if item and (item.IsSlotUsed == 1) and (item.Name == itemName) then
+		for _, setName in pairs(setList) do
+			local bzSetItemIDs = GetEquipmentSetItemIDs(setName)
+			for _, bzItemID in pairs(bzSetItemIDs) do --note: do not change this to ipairs() or it will abort scanning at empty slots in a set
+				if itemID == bzItemID then --loose match: compare the current item's baseID to the baseID of the set item
 					return true
 				end
 			end
 		end
-	end

-	return false
+		return false
+	end
 end

-ItemSearch:RegisterTypedSearch{
+Lib:RegisterTypedSearch{
 	id = 'equipmentSet',
+	tags = {'set'},

-	isSearch = function(self, search)
-		return search and search:match('^s:(.+)$')
+	canSearch = function(self, operator, search)
+		return not operator and search
 	end,

-	findItem = function(self, itemLink, search)
-		local setName = findEquipmentSetByName(search)
-		if not setName then
-			return false
-		end
-
-		return isItemInEquipmentSet(itemLink, setName)
-			or isItemInWardrobeSet(itemLink, setName)
+	findItem = function(self, itemLink, _, search)
+		--this is an item-set search and we know that the only items that can possibly match will be *equippable* items, so we'll short-circuit the response for non-equippable items to speed up searches.
+		if not IsEquippableItem(itemLink) then return false end
+
+		--default to matching *all* equipment sets if no set name has been provided yet
+		if search == '' then search = '*' end
+
+		--generate a list of all equipment sets whose names begin with the search term (or a single set if an exact set name match is found), then look for our item in those equipment sets
+		local setList = {}
+		ES_FindSets(setList, search, true)
+		if #setList == 0 then return false end
+		return ES_CheckItem(itemLink, setList)
 	end,
 }
\ No newline at end of file