Jump to content

Module:Feedback: Difference between revisions

From The Petit Planet Wiki
ReisuDesign (talk | contribs)
No edit summary
ReisuDesign (talk | contribs)
No edit summary
Line 32: Line 32:
user
user
)
)
end
local function escapeAttribute(value)
value = tostring(value or '')
return mw.text.nowiki(value)
end
end


Line 50: Line 43:
local resolved = (entry.status == 'resolved')
local resolved = (entry.status == 'resolved')


local user = entry.user or 'Anonymous'
local user =
local feedback = entry.feedback or 'No feedback provided'
entry.user
local page = entry.page
or 'Anonymous'
local id = entry.id
 
local resolvedBy = entry.resolvedby
local feedback =
local resolvedDate = entry.resolveddate
entry.feedback
or 'No feedback provided'
 
local page =
entry.page
 
local id =
mw.text.trim(
tostring(entry.id or '')
)


local resolvedBy =
mw.text.trim(
tostring(entry.resolvedby or '')
)
local resolvedDate =
mw.text.trim(
tostring(entry.resolveddate or '')
)


local root = mw.html.create('div')
local root = mw.html.create('div')
Line 65: Line 76:
or 'feedback-box--unresolved'
or 'feedback-box--unresolved'
)
)


--------------------------------------------------------
--------------------------------------------------------
-- Metadata
-- Meta
--------------------------------------------------------
--------------------------------------------------------


local meta = root:tag('div')
local meta = root:tag('div')
:addClass('feedback-meta')
:addClass('feedback-meta')


if page and page ~= '' then
if page and page ~= '' then
meta:tag('span')
meta:tag('span')
:addClass('feedback-page')
:addClass('feedback-page')
:wikitext(
:wikitext(
string.format('[[%s]]', page)
string.format(
'[[%s]]',
page
)
)
)


Line 86: Line 97:
:addClass('feedback-separator')
:addClass('feedback-separator')
:wikitext(' • ')
:wikitext(' • ')
end
end


meta:tag('span')
meta:tag('span')
:addClass('feedback-user')
:addClass('feedback-user')
:wikitext(formatUser(user))
:wikitext(
 
formatUser(user)
)


meta:tag('span')
meta:tag('span')
:addClass('feedback-separator')
:addClass('feedback-separator')
:wikitext(' • ')
:wikitext(' • ')


meta:tag('span')
meta:tag('span')
Line 112: Line 121:
or 'Unresolved'
or 'Unresolved'
)
)


--------------------------------------------------------
--------------------------------------------------------
-- Feedback text
-- Feedback
--------------------------------------------------------
--------------------------------------------------------


Line 121: Line 129:
:addClass('feedback-body')
:addClass('feedback-body')
:wikitext(feedback)
:wikitext(feedback)


--------------------------------------------------------
--------------------------------------------------------
Line 127: Line 134:
--------------------------------------------------------
--------------------------------------------------------


if resolved and resolvedBy and resolvedBy ~= '' then
if resolved and resolvedBy ~= '' then


local resolutionMeta =
local resolution = root:tag('div')
'Resolved by ' .. formatUser(resolvedBy)
:addClass('feedback-resolution')


if resolvedDate and resolvedDate ~= '' then
resolution:wikitext(
resolutionMeta =
'Resolved by '
resolutionMeta
.. formatUser(resolvedBy)
.. ' on '
)
.. resolvedDate
 
if resolvedDate ~= '' then
resolution:wikitext(
' on '
.. mw.text.nowiki(resolvedDate)
)
end
end
root:tag('div')
:addClass('feedback-resolution')
:wikitext(resolutionMeta)
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Resolution controls
-- Resolve instruction
--------------------------------------------------------
--------------------------------------------------------


Line 155: Line 161:
:addClass('feedback-instruction')
:addClass('feedback-instruction')


if id ~= '' then


instruction:tag('span')
instruction:wikitext(
:wikitext(
'If this feedback has been resolved, click the button below. '
'If this feedback has been resolved, '
.. 'click the button below.'
)
)
if id and id ~= '' then


instruction:tag('button')
instruction:tag('button')
:addClass('feedback-resolve-button')
:addClass('feedback-resolve-button')
:attr('type', 'button')
:attr('type', 'button')
:attr('data-feedback-id', escapeAttribute(id))
:attr('data-feedback-id', id)
:wikitext('Resolve')
:wikitext('Resolve')
else
instruction:wikitext(
'Add an ID to this feedback to enable the Resolve button.'
)


end
end
end
end


return tostring(root)
return tostring(root)
Line 190: Line 196:
end
end


 
local bucket =
local bucket = mw.ext.bucket('feedback')
mw.ext.bucket('feedback')


if not bucket then
if not bucket then
return false, 'Feedback bucket could not be opened'
return false, 'Feedback bucket could not be opened'
end
end


local success, err = pcall(function()
local success, err = pcall(function()
Line 211: Line 216:


end)
end)


if not success then
if not success then
return false, tostring(err)
return false, tostring(err)
end
end


return true
return true
Line 224: Line 227:
------------------------------------------------------------
------------------------------------------------------------
-- Main
-- Main
--
-- Used by Template:Feedback
--
-- Example:
--
-- {{Feedback
--  |feedback=Something could be improved here.
--  |user=Rei
--  |resolved=false
--  |id=1758012345678
--  |resolvedby=
--  |resolveddate=
-- }}
------------------------------------------------------------
------------------------------------------------------------


function p.main(frame)
function p.main(frame)


local parent = frame:getParent()
local parent =
frame:getParent()


local args =
local args =
parent and parent.args
parent and parent.args
or frame.args
or frame.args
--------------------------------------------------------
-- Get page title
--------------------------------------------------------


local pageTitle = nil
local pageTitle = nil
Line 256: Line 242:
if parent and parent.getTitle then
if parent and parent.getTitle then


local title = parent:getTitle()
local title =
parent:getTitle()


if title
if title
Line 264: Line 251:
pageTitle = title
pageTitle = title
end
end
end
end


 
return p._main(
return p._main(args, pageTitle)
args,
 
pageTitle
)
end
end


Line 278: Line 265:
args.feedback
args.feedback
or 'No feedback provided'
or 'No feedback provided'


local user =
local user =
Line 284: Line 270:
or 'Anonymous'
or 'Anonymous'


local id =
args.id
or ''
local resolvedBy =
args.resolvedby
or ''
local resolvedDate =
args.resolveddate
or ''


local resolved =
local resolved =
isTruthy(args.resolved)
isTruthy(args.resolved)


local status =
local status =
Line 293: Line 289:
and 'resolved'
and 'resolved'
or 'unresolved'
or 'unresolved'
local id =
mw.text.trim(
tostring(args.id or '')
)
local resolvedBy =
mw.text.trim(
tostring(args.resolvedby or '')
)
local resolvedDate =
mw.text.trim(
tostring(args.resolveddate or '')
)


local currentPage =
local currentPage =
pageTitle
pageTitle
or mw.title.getCurrentTitle().fullText
or mw.title.getCurrentTitle().fullText


--------------------------------------------------------
--------------------------------------------------------
Line 322: Line 298:
--------------------------------------------------------
--------------------------------------------------------


local saved, saveError = saveFeedback({
local saved, saveError =
page = currentPage,
saveFeedback({
user = user,
page = currentPage,
status = status,
user = user,
feedback = feedback
status = status,
})
feedback = feedback,
 
id = id,
resolvedby = resolvedBy,
resolveddate = resolvedDate
})


if not saved then
if not saved then
return '<strong style="color:red;">Feedback bucket error: '
return '<strong style="color:red;">Feedback bucket error: '
.. mw.text.nowiki(saveError or 'Unknown error')
.. mw.text.nowiki(
saveError or 'Unknown error'
)
.. '</strong>'
.. '</strong>'
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Render feedback card
-- Render card
--------------------------------------------------------
--------------------------------------------------------


local card = renderCard({
local card =
 
renderCard({
page = nil,
page = nil,
 
user = user,
user = user,
status = status,
 
feedback = feedback,
status = status,
id = id,
 
resolvedby = resolvedBy,
feedback = feedback,
resolveddate = resolvedDate
 
}, true)
id = id,
 
resolvedby = resolvedBy,
 
resolveddate = resolvedDate
 
}, true)
 


--------------------------------------------------------
--------------------------------------------------------
-- Categorize feedback
-- Categories
--------------------------------------------------------
--------------------------------------------------------


local category
local categories = {}


if resolved then
if resolved then
 
table.insert(
category =
categories,
'[[Category:Resolved Feedback]]'
'[[Category:Resolved Feedback]]'
 
)
else
else
 
table.insert(
category =
categories,
'[[Category:Unresolved Feedback]]'
'[[Category:Unresolved Feedback]]'
 
)
end
end


if mw.text.trim(
tostring(id)
) == '' then


return card .. category
table.insert(
categories,
'[[Category:Feedbacks Missing ID]]'
)
end


return card
.. table.concat(categories)
end
end


Line 395: Line 374:
return '<em>No feedback entries found.</em>'
return '<em>No feedback entries found.</em>'
end
end


local list = {}
local list = {}


for _, row in ipairs(results) do
for _, row in ipairs(results) do
Line 404: Line 381:
table.insert(
table.insert(
list,
list,
renderCard(row, false)
renderCard(
row,
false
)
)
)


end
end


 
return table.concat(
return table.concat(list, '\n')
list,
 
'\n'
)
end
end


Line 421: Line 402:
function p.view(frame)
function p.view(frame)


local parent = frame:getParent()
local parent =
 
frame:getParent()


local args
local args
Line 431: Line 412:
then
then


args = frame.args
args =
frame.args


elseif parent
elseif parent
Line 438: Line 420:
then
then


args = parent.args
args =
parent.args


elseif frame
elseif frame
Line 444: Line 427:
then
then


args = frame.args
args =
frame.args


elseif parent
elseif parent
Line 450: Line 434:
then
then


args = parent.args
args =
parent.args


else
else
Line 458: Line 443:
end
end


 
return p._view(
return p._view(args, frame)
args,
 
frame
)
end
end




function p._view(args, frame)
function p._view(args, frame)
--------------------------------------------------------
-- Check Bucket
--------------------------------------------------------


if not (mw.ext and mw.ext.bucket) then
if not (mw.ext and mw.ext.bucket) then
 
return '<em>Bucket extension is not available.</em>'
return
'<em>Bucket extension is not available.</em>'
 
end
end


 
frame =
frame = frame or mw.getCurrentFrame()
frame
 
or mw.getCurrentFrame()
 
--------------------------------------------------------
-- Determine requested status
--------------------------------------------------------


local rawStatus =
local rawStatus =
args.status
args.status
or args[1]
or args[1]


local filterStatus =
local filterStatus =
Line 494: Line 468:
and mw.text.trim(rawStatus):lower()
and mw.text.trim(rawStatus):lower()
or nil
or nil


--------------------------------------------------------
--------------------------------------------------------
-- Specific status
-- Filtered views
--------------------------------------------------------
--------------------------------------------------------


Line 506: Line 479:
local query =
local query =
mw.ext.bucket('feedback')
mw.ext.bucket('feedback')


query.select(
query.select(
Line 512: Line 484:
'user',
'user',
'status',
'status',
'feedback'
'feedback',
'id',
'resolvedby',
'resolveddate'
)
)


query.where(
query.where(
Line 520: Line 494:
filterStatus
filterStatus
)
)


return renderFeedbackList(
return renderFeedbackList(
query.run()
query.run()
)
)
end
end


--------------------------------------------------------
--------------------------------------------------------
-- All feedback
-- All
--------------------------------------------------------
--------------------------------------------------------


Line 537: Line 508:
local query =
local query =
mw.ext.bucket('feedback')
mw.ext.bucket('feedback')


query.select(
query.select(
Line 543: Line 513:
'user',
'user',
'status',
'status',
'feedback'
'feedback',
'id',
'resolvedby',
'resolveddate'
)
)


return renderFeedbackList(
return renderFeedbackList(
query.run()
query.run()
)
)
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Tabber mode
-- Default tabbed view
--------------------------------------------------------
--------------------------------------------------------


local qAll =
local qAll =
mw.ext.bucket('feedback')
mw.ext.bucket('feedback')


qAll.select(
qAll.select(
Line 566: Line 535:
'user',
'user',
'status',
'status',
'feedback'
'feedback',
'id',
'resolvedby',
'resolveddate'
)
)


local allEntries =
local allEntries =
qAll.run()
qAll.run()


local allList = {}
local allList = {}
local unresolvedList = {}
local unresolvedList = {}
local resolvedList = {}
local resolvedList = {}
--------------------------------------------------------
-- Split entries by status
--------------------------------------------------------


if allEntries and #allEntries > 0 then
if allEntries and #allEntries > 0 then
Line 588: Line 553:


local card =
local card =
renderCard(row, false)
renderCard(
 
row,
false
)


table.insert(
table.insert(
Line 595: Line 562:
card
card
)
)


if row.status == 'resolved' then
if row.status == 'resolved' then
Line 612: Line 578:


end
end
end
end
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Build tab contents
-- All content
--------------------------------------------------------
--------------------------------------------------------


Line 627: Line 590:


allContent =
allContent =
table.concat(allList, '\n')
table.concat(
allList,
'\n'
)


else
else
Line 636: Line 602:
end
end


--------------------------------------------------------
-- Unresolved content
--------------------------------------------------------


local unresolvedContent
local unresolvedContent
Line 642: Line 611:


unresolvedContent =
unresolvedContent =
table.concat(unresolvedList, '\n')
table.concat(
unresolvedList,
'\n'
)


else
else
Line 651: Line 623:
end
end


--------------------------------------------------------
-- Resolved content
--------------------------------------------------------


local resolvedContent
local resolvedContent
Line 657: Line 632:


resolvedContent =
resolvedContent =
table.concat(resolvedList, '\n')
table.concat(
resolvedList,
'\n'
)


else
else
Line 665: Line 643:


end
end


--------------------------------------------------------
--------------------------------------------------------
Line 671: Line 648:
--------------------------------------------------------
--------------------------------------------------------


local tabberBody = string.format(
local tabberBody =
'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
string.format(
allContent,
'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
unresolvedContent,
allContent,
resolvedContent
unresolvedContent,
)
resolvedContent
 
)


return frame:callParserFunction(
return frame:callParserFunction(
Line 686: Line 663:
}
}
)
)
end
end


return p
return p

Revision as of 05:17, 16 September 2026

Module documentation

This module implements {{Feedback}}.

local p = {}

------------------------------------------------------------
-- Helpers
------------------------------------------------------------

local function isTruthy(v)
	if v == nil then
		return false
	end

	v = mw.text.trim(tostring(v)):lower()

	return v ~= ''
		and v ~= '0'
		and v ~= 'false'
		and v ~= 'no'
		and v ~= 'off'
end


local function formatUser(user)
	user = mw.text.trim(tostring(user or ''))

	if user == '' or user:lower() == 'anonymous' then
		return 'Anonymous'
	end

	return string.format(
		'[[User:%s|%s]]',
		user,
		user
	)
end


------------------------------------------------------------
-- Feedback card
------------------------------------------------------------

local function renderCard(entry, showInstruction)

	local resolved = (entry.status == 'resolved')

	local user =
		entry.user
		or 'Anonymous'

	local feedback =
		entry.feedback
		or 'No feedback provided'

	local page =
		entry.page

	local id =
		mw.text.trim(
			tostring(entry.id or '')
		)

	local resolvedBy =
		mw.text.trim(
			tostring(entry.resolvedby or '')
		)

	local resolvedDate =
		mw.text.trim(
			tostring(entry.resolveddate or '')
		)

	local root = mw.html.create('div')
		:addClass('feedback-box')
		:addClass(
			resolved
				and 'feedback-box--resolved'
				or 'feedback-box--unresolved'
		)

	--------------------------------------------------------
	-- Meta
	--------------------------------------------------------

	local meta = root:tag('div')
		:addClass('feedback-meta')

	if page and page ~= '' then
		meta:tag('span')
			:addClass('feedback-page')
			:wikitext(
				string.format(
					'[[%s]]',
					page
				)
			)

		meta:tag('span')
			:addClass('feedback-separator')
			:wikitext(' • ')
	end

	meta:tag('span')
		:addClass('feedback-user')
		:wikitext(
			formatUser(user)
		)

	meta:tag('span')
		:addClass('feedback-separator')
		:wikitext(' • ')

	meta:tag('span')
		:addClass('feedback-status')
		:addClass(
			resolved
				and 'feedback-status--resolved'
				or 'feedback-status--unresolved'
		)
		:wikitext(
			resolved
				and 'Resolved'
				or 'Unresolved'
		)

	--------------------------------------------------------
	-- Feedback
	--------------------------------------------------------

	root:tag('div')
		:addClass('feedback-body')
		:wikitext(feedback)

	--------------------------------------------------------
	-- Resolution information
	--------------------------------------------------------

	if resolved and resolvedBy ~= '' then

		local resolution = root:tag('div')
			:addClass('feedback-resolution')

		resolution:wikitext(
			'Resolved by '
			.. formatUser(resolvedBy)
		)

		if resolvedDate ~= '' then
			resolution:wikitext(
				' on '
				.. mw.text.nowiki(resolvedDate)
			)
		end
	end

	--------------------------------------------------------
	-- Resolve instruction
	--------------------------------------------------------

	if showInstruction and not resolved then

		local instruction = root:tag('div')
			:addClass('feedback-instruction')

		if id ~= '' then

			instruction:wikitext(
				'If this feedback has been resolved, click the button below. '
			)

			instruction:tag('button')
				:addClass('feedback-resolve-button')
				:attr('type', 'button')
				:attr('data-feedback-id', id)
				:wikitext('Resolve')

		else

			instruction:wikitext(
				'Add an ID to this feedback to enable the Resolve button.'
			)

		end
	end

	return tostring(root)
end


------------------------------------------------------------
-- Save feedback
------------------------------------------------------------

local function saveFeedback(data)

	if not (mw.ext and mw.ext.bucket) then
		return false, 'Bucket extension is not available'
	end

	local bucket =
		mw.ext.bucket('feedback')

	if not bucket then
		return false, 'Feedback bucket could not be opened'
	end

	local success, err = pcall(function()

		bucket.put({
			page = data.page,
			user = data.user,
			status = data.status,
			feedback = data.feedback,
			id = data.id,
			resolvedby = data.resolvedby,
			resolveddate = data.resolveddate
		})

	end)

	if not success then
		return false, tostring(err)
	end

	return true
end


------------------------------------------------------------
-- Main
------------------------------------------------------------

function p.main(frame)

	local parent =
		frame:getParent()

	local args =
		parent and parent.args
		or frame.args

	local pageTitle = nil

	if parent and parent.getTitle then

		local title =
			parent:getTitle()

		if title
			and title ~= ''
			and not title:find('^Template:')
		then
			pageTitle = title
		end
	end

	return p._main(
		args,
		pageTitle
	)
end


function p._main(args, pageTitle)

	local feedback =
		args.feedback
		or 'No feedback provided'

	local user =
		args.user
		or 'Anonymous'

	local id =
		args.id
		or ''

	local resolvedBy =
		args.resolvedby
		or ''

	local resolvedDate =
		args.resolveddate
		or ''

	local resolved =
		isTruthy(args.resolved)

	local status =
		resolved
			and 'resolved'
			or 'unresolved'

	local currentPage =
		pageTitle
		or mw.title.getCurrentTitle().fullText

	--------------------------------------------------------
	-- Save to Bucket
	--------------------------------------------------------

	local saved, saveError =
		saveFeedback({
			page = currentPage,
			user = user,
			status = status,
			feedback = feedback,
			id = id,
			resolvedby = resolvedBy,
			resolveddate = resolvedDate
		})

	if not saved then
		return '<strong style="color:red;">Feedback bucket error: '
			.. mw.text.nowiki(
				saveError or 'Unknown error'
			)
			.. '</strong>'
	end

	--------------------------------------------------------
	-- Render card
	--------------------------------------------------------

	local card =
		renderCard({
			page = nil,
			user = user,
			status = status,
			feedback = feedback,
			id = id,
			resolvedby = resolvedBy,
			resolveddate = resolvedDate
		}, true)

	--------------------------------------------------------
	-- Categories
	--------------------------------------------------------

	local categories = {}

	if resolved then
		table.insert(
			categories,
			'[[Category:Resolved Feedback]]'
		)
	else
		table.insert(
			categories,
			'[[Category:Unresolved Feedback]]'
		)
	end

	if mw.text.trim(
		tostring(id)
	) == '' then

		table.insert(
			categories,
			'[[Category:Feedbacks Missing ID]]'
		)
	end

	return card
		.. table.concat(categories)
end


------------------------------------------------------------
-- Render feedback list
------------------------------------------------------------

local function renderFeedbackList(results)

	if not results or #results == 0 then
		return '<em>No feedback entries found.</em>'
	end

	local list = {}

	for _, row in ipairs(results) do

		table.insert(
			list,
			renderCard(
				row,
				false
			)
		)

	end

	return table.concat(
		list,
		'\n'
	)
end


------------------------------------------------------------
-- View feedback
------------------------------------------------------------

function p.view(frame)

	local parent =
		frame:getParent()

	local args

	if frame
		and frame.args
		and frame.args.status
	then

		args =
			frame.args

	elseif parent
		and parent.args
		and parent.args.status
	then

		args =
			parent.args

	elseif frame
		and frame.args
	then

		args =
			frame.args

	elseif parent
		and parent.args
	then

		args =
			parent.args

	else

		args = {}

	end

	return p._view(
		args,
		frame
	)
end


function p._view(args, frame)

	if not (mw.ext and mw.ext.bucket) then
		return '<em>Bucket extension is not available.</em>'
	end

	frame =
		frame
		or mw.getCurrentFrame()

	local rawStatus =
		args.status
		or args[1]

	local filterStatus =
		rawStatus
		and mw.text.trim(rawStatus):lower()
		or nil

	--------------------------------------------------------
	-- Filtered views
	--------------------------------------------------------

	if filterStatus == 'resolved'
		or filterStatus == 'unresolved'
	then

		local query =
			mw.ext.bucket('feedback')

		query.select(
			'page',
			'user',
			'status',
			'feedback',
			'id',
			'resolvedby',
			'resolveddate'
		)

		query.where(
			'status',
			filterStatus
		)

		return renderFeedbackList(
			query.run()
		)
	end

	--------------------------------------------------------
	-- All
	--------------------------------------------------------

	if filterStatus == 'all' then

		local query =
			mw.ext.bucket('feedback')

		query.select(
			'page',
			'user',
			'status',
			'feedback',
			'id',
			'resolvedby',
			'resolveddate'
		)

		return renderFeedbackList(
			query.run()
		)
	end

	--------------------------------------------------------
	-- Default tabbed view
	--------------------------------------------------------

	local qAll =
		mw.ext.bucket('feedback')

	qAll.select(
		'page',
		'user',
		'status',
		'feedback',
		'id',
		'resolvedby',
		'resolveddate'
	)

	local allEntries =
		qAll.run()

	local allList = {}
	local unresolvedList = {}
	local resolvedList = {}

	if allEntries and #allEntries > 0 then

		for _, row in ipairs(allEntries) do

			local card =
				renderCard(
					row,
					false
				)

			table.insert(
				allList,
				card
			)

			if row.status == 'resolved' then

				table.insert(
					resolvedList,
					card
				)

			else

				table.insert(
					unresolvedList,
					card
				)

			end
		end
	end

	--------------------------------------------------------
	-- All content
	--------------------------------------------------------

	local allContent

	if #allList > 0 then

		allContent =
			table.concat(
				allList,
				'\n'
			)

	else

		allContent =
			'<em>No feedback entries found.</em>'

	end

	--------------------------------------------------------
	-- Unresolved content
	--------------------------------------------------------

	local unresolvedContent

	if #unresolvedList > 0 then

		unresolvedContent =
			table.concat(
				unresolvedList,
				'\n'
			)

	else

		unresolvedContent =
			'<em>No unresolved feedback entries found.</em>'

	end

	--------------------------------------------------------
	-- Resolved content
	--------------------------------------------------------

	local resolvedContent

	if #resolvedList > 0 then

		resolvedContent =
			table.concat(
				resolvedList,
				'\n'
			)

	else

		resolvedContent =
			'<em>No resolved feedback entries found.</em>'

	end

	--------------------------------------------------------
	-- Tabber
	--------------------------------------------------------

	local tabberBody =
		string.format(
			'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
			allContent,
			unresolvedContent,
			resolvedContent
		)

	return frame:callParserFunction(
		'#tag',
		{
			'tabber',
			tabberBody
		}
	)
end

return p