Mô đun:GHS phrases

--------------------------------------------------------------------------------
-- Module:GHS phrases
-- 
-- main: reads GHS parameters (arguments like "H301", "P401")
--		and returns for each (listtype='abbr'):
--		phraseID visible; formal phrase text as 
-- setID	= "H" or "P"
-- phraseID = e.g. "H201", "P231+P234"
-- phrase text read from array tables in Module:GHS phrases/data
--
-- Implements: Template:GHS phrases
-- Helppage: Template:GHS phrases
-- Error category: , Thể loại:Lỗi GHS (mainspace pages only)
--
-- Also: 
-- listAll(), numberOfPhrases(), listOmitRules(),
-- listtype, omit
--------------------------------------------------------------------------------
require('strict')
local r = {}	-- "r" for return, so no confusion with setID P
local GHSdata	= mw.loadData('Module:GHS phrases/data')
local getArgs	= require('Module:Arguments').getArgs
local tTools	= require('Module:TableTools')
local yesno		= require('Module:Yesno')
local tArgName	= {} -- named parameters (setid, omit, listtype)
local tMessagesToShow = {} -- the tail: Preview, Categories
--------------------------------------------------------------------------------
-- wlHelpPage
--
-- Formats page as Label
-- by default, sLabel == sSection
--------------------------------------------------------------------------------
local function wlHelpPage(sSection, sLabel)
local sHelpPage = 'Template:GHS phrases'
	
	if sLabel == nil then sLabel = sSection end
	
	if (sLabel or ) ==  then
		sLabel = 
	else
		sLabel = '|' .. sLabel 
	end
	if (sSection or ) ==  then
		sSection = 
	else
		sSection = '#' .. sSection
	end
	return  .. sHelpPage .. sSection .. sLabel .. 
end
--------------------------------------------------------------------------------
-- addErrorCategory
--
-- Formats as 
-- or  when in other namespace.
-- sCatsort option using: H, P, _
--------------------------------------------------------------------------------
local function addErrorCategory(sCatsort)
local pagetype = require('Module:Pagetype').main
	
	local wlErrCat = 
	if pagetype() == 'article' then -- mainspace only
		if sCatsort == nil then sCatsort = tArgName'setID' end
		
		if sCatsort ==  then
			wlErrCat = 'Thể loại:Lỗi GHS'
		else
			wlErrCat =  .. sCatsort .. 
		end
	else
		return 
	end
	table.insert(tMessagesToShow, wlErrCat)
	return
end
--------------------------------------------------------------------------------
-- addWarningCategory
--
-- Formats as catsort
-- mainspace only, or  when in other namespace.
-- sCatsort option using: H, P, U, ?, D, O
--------------------------------------------------------------------------------
local function addWarningCategory(sCatsort)
local pagetype = require('Module:Pagetype').main
if sCatsort == nil then sCatsort = tArgName'setID' end
		
	local wlWarnCat = 
	if pagetype() == 'article' then -- mainspace only
		if sCatsort ==  then
			wlWarnCat = 'Thể loại:Lỗi GHS'
		else
			wlWarnCat =  .. sCatsort .. 
		end
	else
		return 
	end
	table.insert(tMessagesToShow, wlWarnCat)
	return
end
--------------------------------------------------------------------------------
-- addPreviewMsg
--------------------------------------------------------------------------------
local function addPreviewMsg(sMsg)
local previewWarn = require('Module:If preview')._warning
	table.insert(tMessagesToShow, previewWarn({sMsg}))
	return
end
--------------------------------------------------------------------------------
-- showPreviewMsg
--
-- show table tMessagesToShow
-- preview-messages and errorcat
-- all namespaces
--------------------------------------------------------------------------------
local function showPreviewMsg()
	if tTools.size(tMessagesToShow) > 0 then
		return table.concat(tMessagesToShow, )
	else
		return 
	end
end
--------------------------------------------------------------------------------
-- applyRemoveDuplicates
--
-- returns edited table, with double Codes removed
-- adds warning with codes.
-- base table tArgs is walked through by a iwalker that reads a singel code,
-- then a ikiller checks the upward part of the same table to delete all copies
-- ikiller starts at end of table, walks towards iwalker; then tArgs is compressed
-- iwalker steps 1 up in the freshly compressed table
-- Used: iArgs is sorted, and order stays same. compress does not change that.
--------------------------------------------------------------------------------
local function applyRemoveDuplicates(tArgs)
local iR, iK -- iR = reader, iK = killer
local hit = false
	iR = 1
	while iR  iR do
			if tArgsiK == tArgsiR then
				hit = true
				addPreviewMsg('Duplicate removed: ' .. tArgsiR)
				table.remove(tArgs, iK)
				tTools.compressSparseArray(tArgs)
			end
			iK = iK - 1
		end
		tTools.compressSparseArray(tArgs)
		iR = iR + 1
	end
	if hit then
		addWarningCategory('D')
	end
	return tArgs
end
--------------------------------------------------------------------------------
-- applyOmitRules
--
-- returns edited table, with Omit phraseID's removed
-- Omit rule is per GHS_Rev9E_0.pdf (2021)
--------------------------------------------------------------------------------
local function applyOmitRules(tArgs)
local tRules = GHSdata'tOmitRules'
	local hit = false
	
	for keep, omit in pairs(tRules) do
		if tTools.inArray(tArgs, omit) then
			if tTools.inArray(tArgs, keep) then
				hit = true
				for i, k in pairs(tArgs) do
					if k == omit then
						table.remove(tArgs, i)		
					end
				end
				addPreviewMsg(wlHelpPage('Omit Rules') .. ': keep ' .. keep .. ', omit ' .. omit)
			end
		end
	end
	if hit then
		tTools.compressSparseArray(tArgs)
		addWarningCategory('O')
	end
	return tArgs
end
--------------------------------------------------------------------------------
-- label H-phrases or P-phrases
--------------------------------------------------------------------------------
local function PHlabel()
	if tArgName'setID' == 'GHS' then
		return 'GHS phrases'
	else
		return tArgName'setID' .. '-phrases'
	end
end
--------------------------------------------------------------------------------
-- inMono
--
-- Use mono font-family (from: Template:Mono)
--------------------------------------------------------------------------------
local function inMono(s)
	if s == nil then s =  end
	return  .. s .. 
end
--------------------------------------------------------------------------------
-- wlInlineTag
--
-- Returns ? with wikilink to errormessage
--------------------------------------------------------------------------------
local function wlInlineTag(phraseID)
	local sMsg
	sMsg = 
				.. wlHelpPage(PHlabel(), '?')
				.. 
	return sMsg
end
--------------------------------------------------------------------------------
-- errorPhraseIDnotFound
--
-- Returns single value when error (not found in list):
-- plain value + inline warning ? (linked) + error cat (mainsp) + preview warning
--------------------------------------------------------------------------------
local function errorPhraseIDnotFound(phraseID)
	if phraseID == nil then phraseID =  end
	
	local inlineTag = wlInlineTag(phraseID)
	local previewMsg = wlHelpPage(PHlabel()) .. ': không tìm thấy \"' .. phraseID .. '\"'
	addPreviewMsg(previewMsg)
	addErrorCategory()
	
	return phraseID .. inlineTag
end
--------------------------------------------------------------------------------
-- errorHPsetIDnotFound
--
-- setID H or P could not be found
--------------------------------------------------------------------------------
local function errorHPsetIDnotFound()
	local sMsg
	sMsg = wlHelpPage(, PHlabel())
			.. ': không tìm thấy id "H" hoặc "P"' 
			.. ' (vui lòng sử dụng mẫu như "|H200" hoặc "|P300+P301")'
	addPreviewMsg(sMsg)
	addErrorCategory('?')
	return showPreviewMsg()
end
--------------------------------------------------------------------------------
-- errorHPsetIDmissing
--
-- parameter |setid= to be used
--------------------------------------------------------------------------------
local function errorHPsetIDmissing()
	local sMsg
	sMsg = wlHelpPage( , PHlabel())
			.. ': không tìm thấy id "H" hoặc "P",' 
			.. ' vui lòng sử dụng |setid=... (H hoặc P)'
	addPreviewMsg(sMsg)
	return
end
--------------------------------------------------------------------------------
-- formatPhraseAbbr
--
-- format phraseID and text, for abbr-form (infobox list form)
--------------------------------------------------------------------------------
local function formatPhraseAbbr(phraseID, sPhrase)
	return 
				.. phraseID 
				.. 
end
--------------------------------------------------------------------------------
-- formatPhraseInline
--
-- format phraseID and text, for inline form (in sentence)
-- adds "quotes"
--------------------------------------------------------------------------------
local function formatPhraseInline(phraseID, sPhrase)
	return inMono(phraseID) .. ': \"' .. sPhrase .. '\"'
end
--------------------------------------------------------------------------------
-- formatPhraseList
--
-- as inline, but no "quotes" added.
--------------------------------------------------------------------------------
local function formatPhraseList(phraseID, sPhrase)
	return inMono(phraseID) .. ': ' .. sPhrase
end
--------------------------------------------------------------------------------
-- getSetID
--
-- Determines setID (expected either 'H' or 'P')
-- First route is: read |setid=
-- When |setid= is not set, 
--		it looks for a first parameter that has an H of P prefix (in |P201|P202|...)
--		when not found, 'GHS' is retured
-- In one call, P and H numbers can *not* be mixed
--		so "|H201|P202|" will cause error "P202 not found" (... in H-list)
--------------------------------------------------------------------------------
local function getSetID(tArgs)
	local setIDfound = 'GHS'
	local paramsetID = tArgs'setid' or nil
	
	if (paramsetID ~= nil) and (paramsetID == 'P' or paramsetID == 'H') then
		setIDfound = paramsetID
	else
		local initial = nil
		for i, v in ipairs(tArgs) do
			initial = mw.ustring.match(v, '^PH')
			if initial ~=nil then
				setIDfound = initial
				break
			end
		end
	end
	return setIDfound
end
--------------------------------------------------------------------------------
-- getListType
--
-- Checks list format, including those from Module:List
--------------------------------------------------------------------------------
local function getListType(tArgs)
	local listTypes = {
	'abbr' = true,
	'bulleted' = true,
	'unbulleted' = true,
	'horizontal' = true,
	'ordered' = true,
	'horizontal_ordered' = true,
	'horizontal ordered' = true,
	'inline' = true
	}
	local sListType = tArgs'listtype' or 'abbr'
	if sListType ==  or sListType == 'abbr' then
		return 'abbr'
	elseif listTypessListType == true then
		if sListType == 'horizontal ordered' then
			sListType = 'horizontal_ordered'
		end
		return sListType
	else 
		sListType = 'abbr'
	end
	return sListType
end
--------------------------------------------------------------------------------
-- getDoOmitRules
--------------------------------------------------------------------------------
local function getDoOmitRules(tArgs)
	local b = yesno(tArgs'omit', true)
	
	if b == nil then b = true end
	return yesno(b, true)
end
--------------------------------------------------------------------------------
-- prepareArgs
--
-- First: determine setID (from |setID= OR from prefixes in parameters)
-- Then: clean up & format phrase IDs (=unnamed parameters)
--		remove bad characters, create H/P pattern "H201", "P310+P302"
-- straight array, no nil's, sorted
--------------------------------------------------------------------------------
local function prepareArgs(tArgs)
	tArgName'setID' = getSetID(tArgs)
	tArgName'listtype' = getListType(tArgs)
	tArgName'omit' = getDoOmitRules(tArgs)
	tArgs = tTools.compressSparseArray(tArgs) -- removes all named args
	if string.len(tArgName'setID') == 1 and #tArgs > 0 then
		for i, v in ipairs(tArgs) do
			v = mw.text.decode(v)
			v = mw.ustring.gsub(v, '^%d%+A-Za-z', )
			v = mw.ustring.gsub(v, '^(%d)', tArgName'setID' .. '%1')
			v = mw.ustring.gsub(v, '%+(%d)', '+' .. tArgName'setID' .. '%1')
			tArgsi = v
		end
		table.sort(tArgs)
	end
	return tArgs
end
--------------------------------------------------------------------------------
-- listAll
--
-- Returns wikitable rows for each phrase id.
-- requires |setID=P/H
-- returns full list, all phrases, for a setID
-- 2-columns wikitable, sorted, sortable, anchor like "H201" for each
--------------------------------------------------------------------------------
function r.listAll(frame)
local newArgs = getArgs(frame)
local tL = {}
	prepareArgs(newArgs)
	
	local tRead
	if tArgName'setID' == 'H' then
		tRead = GHSdata'Hphrases'
	elseif tArgName'setID' == 'P' then
		tRead = GHSdata'Pphrases'
	else 
		errorHPsetIDmissing()
		return  showPreviewMsg()
	end
	-- Intermediate table t2 to maintain order; read from original table (/data)
	local t2 = {}
	local iPh
	for s, v in pairs(tRead) do
		iPh = tonumber(mw.ustring.match(s, 'PH(%d%d%d)'))
		if string.len(s) > 4 then
			iPh = tTools.size(t2) + 1
		end
		table.insert(t2, iPh, s)
	end
	t2 = tTools.compressSparseArray(t2)
	table.sort(t2)
	local sTR, v, sAnchor
	-- i = array index, s = phraseID, v = phrase text
	for i, s in ipairs(t2) do
		v = tReads
		sAnchor = 
		sTR = '
		table.insert(tL, sTR)
	end
	return table.concat(tL, '\n')
end
--------------------------------------------------------------------------------
-- numberOfPhrases
--
-- Documentation
-- requires |setID=H/P
-- Returns number of phrases, in format
--	"GHS H-phrases (123)"
--------------------------------------------------------------------------------
function r.numberOfPhrases(frame)
	local newArgs = getArgs(frame)
	prepareArgs(newArgs)
	local iT
	if tArgName'setID' == 'H' then
		iT = tTools.size(GHSdata'Hphrases')
	elseif tArgName'setID' == 'P' then
		iT = tTools.size(GHSdata'Pphrases')
	else
		errorHPsetIDmissing()
		return showPreviewMsg()
	end
	return 'GHS ' .. PHlabel() .. ' (' .. tostring(iT) .. ')'
end
--------------------------------------------------------------------------------
-- listOmitRules
--
-- self-documentation
--------------------------------------------------------------------------------
function r.listOmitRules()
local tRules = GHSdata'tOmitRules'
local tL = {}
local s
	s = wlHelpPage('Omit Rules')
		.. ': when the keep ID is present, do not show the omit ID phrase'
	table.insert(tL, s)
	for keep, omit in pairs (tRules) do
		s = '• keep ' .. inMono(keep) .. ', omit ' .. inMono(omit)
		table.insert(tL, s)
	end
	return table.concat(tL, )
end
--------------------------------------------------------------------------------
-- _main
--
-- processes setID (H, P) and phrase codes
--		error:	setID not P, H
--				code not found
-- cannot mix H and P phrases
-- reads phrases from /data H or P phrases tables
-- formats phrase (abbreviation, abbr-title, phraseID)
--------------------------------------------------------------------------------
function r._main(tArgs)
	tArgs = prepareArgs(tArgs)
	
	if #tArgs == 0 then
		return showPreviewMsg() -- no content
	elseif tArgName'setID' == 'GHS' then
		return errorHPsetIDnotFound()
	end
	tArgs = applyRemoveDuplicates(tArgs)
	if tArgName'omit' then
		tArgs = applyOmitRules(tArgs)
	end
	local formatterF
	if tArgName'listtype' == 'abbr' then
		formatterF = formatPhraseAbbr
	elseif tArgName'listtype' == 'inline' then
		formatterF = formatPhraseInline
	else --- Module:List options
		formatterF = formatPhraseList
	end
	
	local tReadD = {}
	if tArgName'setID' == 'H' then
		tReadD = GHSdata'Hphrases'
	elseif tArgName'setID' == 'P' then
		tReadD = GHSdata'Pphrases'
	else
		return showPreviewMsg()
	end
	local sPhrase
	local tR = {}
	for i, v in ipairs(tArgs) do
		sPhrase = tReadDv
		if sPhrase == nil then
			table.insert(tR, errorPhraseIDnotFound(tostring(v)))
		else
			table.insert(tR, formatterF(v, sPhrase))
		end
	end
	if tArgName'listtype' == 'abbr' then
		return table.concat(tR, ', ') .. showPreviewMsg()
	elseif tArgName'listtype' == 'inline' then
		return table.concat(tR, ', ') .. showPreviewMsg()
	else
		local mList = require('Module:List')
		return mListtArgName'listtype'(tR) .. showPreviewMsg()
	end
end
--------------------------------------------------------------------------------
-- main
--
-- handles template input frame, then calls generic _main() function
-- To be invoked from 
--------------------------------------------------------------------------------
function r.main(frame)
local newArgs = getArgs(frame)
	return r._main(newArgs) 
end
return r