require('strict')
local p = {}
local getArgs = require('Module:Arguments').getArgs
local str = require('Module:String')
local yesno = require('Module:Yesno')
local defaultSep = ','
local iMaxWords = 16
local warningIMaxWordsReached = nil
local xpLitWordCount = 0
local report -- to be initinated when explain needed  
-- Initialise the /report subpage.
-- only invoked when 'explain' asked
local function initReport
report = require('Module:Str find word/report')
end  
-- Turn "&#x0041;" into "A" etc. asap
-- and reduce multi-spaces (including nbsp etc.) into single space
local function decodeUnicode(str)
return mw.ustring.gsub(mw.text.decode(str), '%s+', ' ')
end  
-- %-Escape any word (character string) before feeding it into a string pattern function
-- all punctuation (%p) will be %-escaped
local function escape_word(word)
return str._escapePattern(word)
end  
-- Reads and parses a word list and returns a table with words (simple array)
-- words list can be: source, andwords-to-check, orwords-to-check
-- step 1: when case-insensitive, turn string into lowercase
-- step 2: read & remove Literals ("..")
-- step 3: read comma-separated words
-- step 4: when booleans=T, change boolean words into true/false (module:yesno rules)
-- all words returned are trimmed, TODO and all ws into single-plainspace?
-- only T/F words are edited, other words remain, untouched
-- return the table (a straight array)
local function buildWordTable(tArgs, sWordlist)
local wordTable = {}
local hitWord = ''
local hitCount = 0
if sWordlist == '' then return wordTable end  
-- Step 1: case-sensitive
if yesno(tArgs.case, true) == false then
sWordlist = string.lower(sWordlist)
end  
-- Step 2: read "literals",
-- then remove them from the string:
-- replaced by single comma; idle & keeps word separation
--- if yesno(tArgs.literals, false) then
if false then
local _, sCount
_, sCount = mw.ustring.gsub(sWordlist, '"', '')
if sCount > 1 then
local litWord = ''
local i, j  
while sCount > 1 do -- could do here: only when even?
i = string.find(sWordlist, '%"', 1, false)
j = string.find(sWordlist, '%"', i+1, false)
litWord = mw.text.trim(string.sub(sWordlist, i+1, j-1))
if #litWord > 0 then -- not an empty string or spaces only
xpLitWordCount = xpLitWordCount + 1
table.insert(wordTable, litWord)
end
-- remove from source, and do next gsub search:
sWordlist = string.gsub(sWordlist, '%"%s*'
.. escape_word(litWord)
.. '%s*%"', ',')
_, sCount = mw.ustring.gsub(sWordlist, '"', '')
end
end
end  
-- Step 3: parse comma-delimited words
hitCount = 0
sWordlist = tArgs.sep .. sWordlist .. tArgs.sep
local eSep
eSep = escape_word(tArgs.sep)
local patstring = '%f[^' .. eSep .. '][^' .. eSep .. ']+%f[' .. eSep .. ']'
if yesno(tArgs.explain, false) then
report.xpMessage('1.eSep: ' .. eSep) -- dev
report.xpMessage('2.pattern: ' .. patstring) -- dev
end
while hitCount <= iMaxWords do
hitCount = hitCount + 1  
hitWord = str._match(sWordlist, patstring, 1, hitCount, false, tArgs.sep)
hitWord = mw.text.trim(hitWord)
if hitWord == tArgs.sep then
-- no more words found in the string
break
elseif hitWord ~= '' then
table.insert(wordTable, hitWord)
end
end
if hitCount > iMaxWords then
warningIMaxWordsReached = 'Max number of words (' .. tostring(iMaxWords) .. ') reached. Extra words are ignored.'
.. ' (' .. mw.ustring.sub(mw.text.trim(sWordlist), 1, 90) .. '&nbsp;...). '
end  
-- Step 4: when read booleans, converse words to true/false
-- todo: check parameter here not elsewhere
if tArgs.booleans then -- TODO if Yesno(tArgs.booleans) ...
local sBool
for i, v in ipairs(wordTable) do
sBool = yesno(v)
if sBool ~= nil then
wordTable[i] = tostring(sBool)
end
end
end  
return wordTable
end  
-- Check whether a single word is in a table (a simple array of words)
-- returns hitword or nil
local function findWordInTable(sourceWordTable, word)
local bHit = false
for i, v in ipairs(sourceWordTable) do
if v == word then
bHit = true
break
end
end
if bHit then
return word
else
return nil
end
end  
-- AND-logic with andWordTable words: ALL words must be found
-- returns {T/F, hittable}
-- T when *all* AND words are found
-- hittable with all hit words
-- note 1: when F, the hittable still contains the words that were found
-- note 2: empty AND-wordlist => True by logic (because: not falsified)
local function checkANDwords(sourceWordTable, andWordTable)
local result1
local bAND
local tHits  
bAND = true
tHits = {}
result1 = nil
if #andWordTable > 0 then
for i, word in ipairs(andWordTable) do
result1 = findWordInTable(sourceWordTable, word) or nil
if result1 == nil then
bAND = false -- Falsified!
-- could break after this logically but
-- continue to complete the table (bAND remains false)
else
table.insert(tHits, result1)
end
end
else
bAND = true
end  
return bAND, tHits
end  
-- OR-logic with orWordTable words: at least one word must be found
-- returns {T/F, hittable}
-- True when at least one OR word is found
-- hittable has all hit words
-- note 1: empty OR-wordlist => True by logic (because: not falsified)
-- note 2: while just one hitword is a True result, the hittable contains all words found
local function checkORwords(sourceWordTable, orWordTable)
local result1
local bOR
local tHits  
bOR = false
tHits = {}
result1 = nil
if #orWordTable > 0 then
for i, word in ipairs(orWordTable) do
result1 = findWordInTable(sourceWordTable, word) or nil
if result1 == nil then
-- this one is false; bOR unchanged; do next
else
bOR = true -- Confirmed!
table.insert(tHits, result1)
-- could break here logically, but complete the check
end
end
else
bOR = true
end  
return bOR, tHits
end  
-- Determine the requested return value (string).
-- sYeslist is the _main return value (logically defined value)
-- this function applies tArgs.yes / tArgs.no return value
-- note: yes='' implies: blank return value
-- note: no parameter yes= (that is, yes=nil) implies: by default, return the sYeslist
local function yesnoReturnstring(tArgs, sYeslist)
if sYeslist == '' then -- False
return tArgs.no or ''
else -- True
if tArgs.yes == nil then
return sYeslist
else -- some |yes= value is entered, could be ''
return tArgs.yes
end
end
end  
local function isPreview
local ifPreview = require('Module:If preview')
return not (ifPreview._warning( {'is_preview'} ) == '')
end  
-- Explain options (=report info), interprets parameter explain=
-- returns true/false/'testcases'
-- explain=true => show report in Preview
-- explain=testcases => WHEN in ns: template: or user: AND subpage = '/testcases' THEN show permanently
local function checkExplain(tArgs)
return false -- never. 22Mar2023 checkExplain(newArgs)
end  
-- ===== ===== ===== ===== ===== ===== ===== ===== ===== ===== ===== ===== =====
-- _main function: check for presence of words in source string
-- Checks and returns:
-- when T: the string of all hitwords (default), or the |yes=... input
-- when F: empty string '' (default), or the |no=... input
-- steps:
-- 1. input word strings are prepared (parsed into an array of words)
-- 2. words checks are made (applying AND-logic, OR-logic)
-- 3. final conclusion drawn (T/F)
-- 4. optionally, the preview report is prepared (debug, feedback)
-- 5. based on T or F status, the return value (string) is established and returned
-- note 1: each return value (yes=.., no=..) can be '' (nulstring)
function p._main(tArgs)
local sourceWordTable = {}
local andWordTable = {}
local orWordTable = {}
local tANDhits
local tORhits
-- logical finding:
local bANDresult = false
local bORresult = false
local resultALL = false
local sYeslist = ''  
sourceWordTable = buildWordTable(tArgs, tArgs.source)
andWordTable = buildWordTable(tArgs, tArgs.andString)
orWordTable = buildWordTable(tArgs, tArgs.orString)  
if (#sourceWordTable == 0) or (#andWordTable + #orWordTable == 0) then
-- No words to check
resultALL = false
if yesno(tArgs.explain, false) then
report.xpNoWords(tArgs, sourceWordTable, andWordTable, orWordTable)
end
else