Documentation for this module may be created at Module:Infobox mapframe/doc

local mf = require('Module:Mapframe')
local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
local infoboxImage = require('Module:InfoboxImage').InfoboxImage

-- Defaults
local DEFAULT_FRAME_WIDTH = "270"
local DEFAULT_FRAME_HEIGHT = "200"
local DEFAULT_ZOOM = 10
local DEFAULT_GEOMASK_STROKE_WIDTH = "1"
local DEFAULT_GEOMASK_STROKE_COLOR = "#777777"
local DEFAULT_GEOMASK_FILL = "#888888"
local DEFAULT_GEOMASK_FILL_OPACITY = "0.25"
local DEFAULT_SHAPE_STROKE_WIDTH = "2"
local DEFAULT_SHAPE_STROKE_COLOR = "#FF0000"
local DEFAULT_SHAPE_FILL = "#606060"
local DEFAULT_SHAPE_FILL_OPACITY = "0.1"
local DEFAULT_LINE_STROKE_WIDTH = "5"
local DEFAULT_LINE_STROKE_COLOR = "#FF0000"
local DEFAULT_MARKER_COLOR = "#5E74F3"

local util = {}

function util.noop(info)
	local DEFAULT_NOOP_OUTPUT = ""
	-- Uncomment this when debugging
	-- DEFAULT_NOOP_OUTPUT = "debug: mapframe no-op: " .. info
	-- mw.log(DEFAULT_NOOP_OUTPUT)
	return DEFAULT_NOOP_OUTPUT
end

-- Trim whitespace from args, and remove empty args
function util.trimArgs(argsTable)
	local cleanArgs = {}
	for key, val in pairs(argsTable) do
		if type(val) == 'string' then
			val = val:match('^%s*(.-)%s*$')
			if val ~= '' then
				cleanArgs[key] = val
			end
		else
			cleanArgs[key] = val
		end
	end
	return cleanArgs
end

function util.shouldAutoRun(frame)
	-- Check if should be running
	local pargs = frame.getParent(frame).args
	local explicitlyOn = yesno(
		mw.text.trim(pargs.mapframe or "")
	)
	if pargs.coordinates == "{{{coordinates}}}" then
		explicitlyOn = false
	end
	if type(pargs.coordinates) == 'string'
		and string.match(pargs.coordinates, 'coord.missing') then
		explicitlyOn = false
	end
	local onByDefault = (explicitlyOn == nil)
		and yesno(
			mw.text.trim(frame.args.onByDefault or ""),
			false
		)
	return explicitlyOn or onByDefault
end

function util.argsFromAuto(frame)
	-- Get args from the frame (invoke call) and the parent (template call).
	-- Frame arguments are default values which are overridden by parent values
	-- when both are present.
	local args = getArgs(frame, {
		parentFirst = true
	})
	-- Discard args not prefixed with "mapframe-", remove that prefix
	-- from those that remain.
	local fixedArgs = {}
	for name, val in pairs(args) do
		local fixedName = string.match(
			name,
			"^mapframe%-(.+)$"
		)
		if fixedName then
			fixedArgs[fixedName] = val
		-- Allow coord, coordinates, etc. to be unprefixed.
		elseif (name == "coordinates"
			or name == "coord"
			or name == "coordinate")
			and not fixedArgs.coord then
			fixedArgs.coord = val
			if not fixedArgs['frame-coord'] then
				fixedArgs['frame-coord'] = val
			end
		-- Allow id/qid to be unprefixed.
		-- Note: Bharatpedia does not resolve these through Wikibase.
		elseif (name == "id" or name == "qid")
			and not fixedArgs.id then
			fixedArgs.id = val
		-- Allow captionstyle to be unprefixed,
		-- for compatibility with Module:Infobox.
		elseif name == "captionstyle"
			and not fixedArgs.captionstyle then
			fixedArgs.captionstyle = val
		end
	end
	-- Parent-frame fallback: Module:Infobox sometimes expands this invoke
	-- in a context where named article parameters are only on the parent.
	if not fixedArgs.coord then
		local parent = frame:getParent()
		if parent and parent.args then
			local raw = parent.args.coordinates
				or parent.args.coord
				or parent.args.coordinate
				or parent.args['mapframe-coord']
				or parent.args['mapframe-coordinates']
			if type(raw) == 'string' then
				raw = mw.text.trim(raw)
				if raw ~= '' then
					fixedArgs.coord = raw
					fixedArgs['frame-coord'] = fixedArgs['frame-coord'] or raw
				end
			end
		end
	end
	return fixedArgs
end

function util.parseCustomWikitext(customWikitext)
	-- infoboxImage will format an image if given wikitext containing
	-- an image, or else pass through the wikitext unmodified.
	return infoboxImage({
		args = {
			image = customWikitext
		}
	})
end

function util.trackAndWarn(trackingCat, warning)
	local title = mw.title.getCurrentTitle()
	local results =
		title
		and title.namespace == 0
		and trackingCat
		and '[[Category:' .. trackingCat .. ']]'
		or ''
	if warning and title and title.namespace ~= 0 then
		results = results
			.. '<strong class="error">Infobox mapframe: '
			.. mw.text.nowiki(tostring(warning))
			.. '</strong>'
	end
	return results
end

-- Turn any {{Coord}} wikitext or expanded HTML into decimal lat, lon.
function util.parseLatLon(coord)
	if type(coord) ~= 'string' or coord == '' then
		return nil, nil
	end
	local okDecode, decoded = pcall(mw.text.decode, coord)
	coord = okDecode and decoded or coord
	coord = string.gsub(coord, '\127[^\127]*\127', ' ')
	if not pcall(mw.ustring.len, coord) then
		coord = string.gsub(coord, '[\128-\255]', ' ')
	end
	local htmlLat = mw.ustring.match(coord, 'class%s*=%s*["\']latitude["\'][^>]*>([^<]+)')
	local htmlLon = mw.ustring.match(coord, 'class%s*=%s*["\']longitude["\'][^>]*>([^<]+)')
	local function hemMul(val, hem, kind)
		val = tonumber(val)
		if not val then return nil end
		if hem == 'S' or hem == 's' then val = -math.abs(val) end
		if hem == 'W' or hem == 'w' then val = -math.abs(val) end
		if kind == 'lat' and (hem == 'N' or hem == 'n') then val = math.abs(val) end
		if kind == 'lon' and (hem == 'E' or hem == 'e') then val = math.abs(val) end
		return val
	end
	if htmlLat and htmlLon then
		local a = mw.ustring.match(htmlLat, '([+-]?[%.%d]+)')
		local b = mw.ustring.match(htmlLon, '([+-]?[%.%d]+)')
		local ah = mw.ustring.match(htmlLat, '([NSns])')
		local bh = mw.ustring.match(htmlLon, '([EWew])')
		if a and b then
			return hemMul(a, ah, 'lat'), hemMul(b, bh, 'lon')
		end
	end
	local hack = mw.ustring.match(coord, 'params=([%d%._NSEWnswe]+)')
	if hack then
		local parts = mw.text.split(hack, '_', true)
		local nums, hems = {}, {}
		for _, part in ipairs(parts) do
			if part:match('^[NSWEnswe]$') then
				hems[#hems + 1] = part:upper()
			elseif tonumber(part) then
				nums[#nums + 1] = tonumber(part)
			end
		end
		if #hems >= 2 and #nums >= 2 then
			local lat, lon
			if #nums == 2 then
				lat, lon = nums[1], nums[2]
			elseif #nums >= 6 then
				lat = nums[1] + nums[2] / 60 + nums[3] / 3600
				lon = nums[4] + nums[5] / 60 + nums[6] / 3600
			elseif #nums == 4 then
				lat = nums[1] + nums[2] / 60
				lon = nums[3] + nums[4] / 60
			end
			if lat and lon then
				return hemMul(lat, hems[1], 'lat'), hemMul(lon, hems[2], 'lon')
			end
		end
	end
	local plain = string.gsub(coord, '<[^>]*>', ' ')
	plain = string.gsub(plain, '&nbsp;', ' ')
	plain = string.gsub(plain, '%s+', ' ')
	plain = mw.text.trim(plain)
	local body = mw.ustring.match(plain, '{{%s*[Cc]oord%s*|([^}]+)}')
	if body then
		local values = {}
		for value in mw.text.gsplit(body, '|', true) do
			value = mw.text.trim(value)
			if value ~= '' and not mw.ustring.find(value, '=', 1, true) then
				values[#values + 1] = value
			end
		end
		if tonumber(values[1]) and tonumber(values[2]) and not values[3] then
			return tonumber(values[1]), tonumber(values[2])
		end
		if tonumber(values[1]) and values[2] and values[2]:match('^[NSns]$')
			and tonumber(values[3]) and values[4] and values[4]:match('^[EWew]$') then
			return hemMul(values[1], values[2], 'lat'), hemMul(values[3], values[4], 'lon')
		end
		if tonumber(values[1]) and tonumber(values[2]) and tonumber(values[3])
			and values[4] and values[4]:match('^[NSns]$')
			and tonumber(values[5]) and tonumber(values[6]) and tonumber(values[7])
			and values[8] and values[8]:match('^[EWew]$') then
			local lat = tonumber(values[1]) + tonumber(values[2]) / 60 + tonumber(values[3]) / 3600
			local lon = tonumber(values[5]) + tonumber(values[6]) / 60 + tonumber(values[7]) / 3600
			return hemMul(lat, values[4], 'lat'), hemMul(lon, values[8], 'lon')
		end
	end
	local a, ah, b, bh = mw.ustring.match(
		plain,
		'([+-]?[%.%d]+)%s*°?%s*([NSns])%D+([+-]?[%.%d]+)%s*°?%s*([EWew])'
	)
	if a and b then
		return hemMul(a, ah, 'lat'), hemMul(b, bh, 'lon')
	end
	local x, y = mw.ustring.match(plain, '([+-]?[%.%d]+)%s*[;,]%s*([+-]?[%.%d]+)')
	if x and y then
		return tonumber(x), tonumber(y)
	end
	x, y = mw.ustring.match(plain, '([+-]?[%.%d]+)%s+([+-]?[%.%d]+)')
	if x and y then
		x, y = tonumber(x), tonumber(y)
		if x and y and x >= -90 and x <= 90 and y >= -180 and y <= 180 then
			return x, y
		end
	end
	return nil, nil
end

function util.resolveCoordPair(val)
	local ok, lat, lon = pcall(util.parseLatLon, val)
	if ok and lat and lon then
		return lat, lon
	end
	if type(val) == 'string' and string.find(val, '{{', 1, true) then
		local frame = mw.getCurrentFrame()
		if frame then
			local ok2, expanded = pcall(function() return frame:preprocess(val) end)
			if ok2 then
				ok, lat, lon = pcall(util.parseLatLon, expanded)
				if ok and lat and lon then
					return lat, lon
				end
			end
		end
	end
	return nil, nil
end

function util.ternary(flag, other)
	other = other or 'other'
	flag = flag == 'none' and 'no' or flag
	local yesNoOut = yesno(flag, other)
	local yes = (yesNoOut == true)
	local no = (yesNoOut == false)
	return yes, no
end

local p = {}

p._caption = function(args)
	if args.caption then
		return args.caption
	elseif args.switcher then
		return util.noop("no caption or switcher")
	end
	-- Bharatpedia does not have Wikidata labels to generate
	-- automatic "Location in ..." captions.
	return util.noop("no caption")
end

p._main = function(_config)
	-- Bharatpedia does not use an external entity database.
	-- All map positioning must therefore come from explicit
	-- template parameters.
	local tracking = ''
	-- `config` is the args passed to this module.
	local config = util.trimArgs(_config)
	-- Allow aliases for coordinates.
	config.coord = config.coord or config.coordinates or config.coordinate
	if config.coord and not config['frame-coord'] and not config['frame-coordinates'] then
		config['frame-coord'] = config.coord
	end
	----------------------------------------------------------------------
	-- COORDINATES
	--
	-- Unlike the Wikimedia version, this module does not look up
	-- coordinates from Wikidata.
	----------------------------------------------------------------------
	if not config.coord
		and not (
			config['frame-coord']
			or config['frame-coordinates']
		)
		and not (
			config['frame-lat']
			and config['frame-long']
		)
		and not (
			config['frame-latitude']
			and config['frame-longitude']
		) then
		return false, util.trackAndWarn(
			'Pages using infobox mapframe with missing coordinates'
		)
	end
	----------------------------------------------------------------------
	-- MAPFRAME ARGUMENTS
	----------------------------------------------------------------------
	local args = {}
	-- Defaults/overrides for infobox presentation.
	args.display = "inline"
	args.frame = "yes"
	args.plain = "yes"
	args["frame-width"] =
		config["frame-width"]
		or config.width
		or DEFAULT_FRAME_WIDTH
	args["frame-height"] =
		config["frame-height"]
		or config.height
		or DEFAULT_FRAME_HEIGHT
	args["frame-align"] = "center"
	args["frame-coord"] =
		config["frame-coordinates"]
		or config["frame-coord"]
		or config.coord
	-- Deprecated latitude/longitude parameters.
	args["frame-lat"] =
		config["frame-lat"]
		or config["frame-latitude"]
	args["frame-long"] =
		config["frame-long"]
		or config["frame-longitude"]
	----------------------------------------------------------------------
	-- ZOOM
	--
	-- Use an explicitly supplied zoom first.
	-- If absent, calculate one from dimensions using Infobox dim.
	-- No Wikidata lookup is performed.
	----------------------------------------------------------------------
	local zoom = config.zoom
	if not zoom then
		local getZoom = require('Module:Infobox dim')._zoom
		zoom = getZoom({
			length_km = config.length_km,
			length_mi = config.length_mi,
			width_km = config.width_km,
			width_mi = config.width_mi,
			area_km2 = config.area_km2,
			area_mi2 = config.area_mi2,
			area_ha = config.area_ha,
			area_acre = config.area_acre,
			type = config.type,
			population = config.population,
			viewport_px = math.min(
				tonumber(args["frame-width"])
					or tonumber(DEFAULT_FRAME_WIDTH),
				tonumber(args["frame-height"])
					or tonumber(DEFAULT_FRAME_HEIGHT)
			)
		})
	end
	args.zoom = zoom or DEFAULT_ZOOM
	----------------------------------------------------------------------
	-- MARKER
	----------------------------------------------------------------------
	local forcePoint, suppressPoint =
		util.ternary(config.point)
	local forceMarker, suppressMarker =
		util.ternary(config.marker, true)
	forcePoint = forcePoint or forceMarker
	suppressPoint = suppressPoint or suppressMarker
	-- With explicit coordinates, show a point marker by default
	-- unless it is explicitly suppressed.
	local showMarker =
		not suppressPoint
		and (
			forcePoint
			or config.coord ~= nil
		)
	----------------------------------------------------------------------
	-- SHAPE / LINE
	--
	-- The Wikimedia version inferred these from Wikidata geometry.
	-- Bharatpedia requires them to be explicitly requested.
	----------------------------------------------------------------------
	local showShape = false
	local showLine = false
	local forceShape, suppressShape =
		util.ternary(config.shape)
	if forceShape then
		showShape = true
	elseif suppressShape then
		showShape = false
	end
	local forceLine, suppressLine =
		util.ternary(config.line)
	if forceLine then
		showLine = true
	elseif suppressLine then
		showLine = false
	end
	local shapeType =
		config.shape == 'inverse'
		and 'shape-inverse'
		or 'shape'
	----------------------------------------------------------------------
	-- GEOMASK / SWITCHER
	----------------------------------------------------------------------
	local maskItem
	if config.switcher == "zooms" then
		-- Switch between zoom levels.
		local maxZoom =
			math.max(
				tonumber(args.zoom) or DEFAULT_ZOOM,
				3
			)
		local minZoom = 1
		local midZoom =
			math.floor(
				(maxZoom + minZoom) / 2
			)
		args.switch =
			"zoomed in, zoomed midway, zoomed out"
		args.zoom =
			string.format(
				"SWITCH:%d,%d,%d",
				maxZoom,
				midZoom,
				minZoom
			)
	elseif config.switcher == "auto" then
		-- The Wikimedia version used Wikidata P276/P131
		-- to determine this automatically.
		--
		-- Bharatpedia has no such dependency, so this mode
		-- is intentionally disabled.
		return false, util.trackAndWarn(
			'Pages using infobox mapframe with unsupported automatic switcher'
		)
	elseif config.switcher == "geomasks"
		and config.geomask then
		-- Explicit geomask IDs are supported.
		args.zoom = nil
		local separator =
			(
				mw.ustring.find(
					config.geomask,
					"###",
					0,
					true
				)
				and "###"
			)
			or
			(
				mw.ustring.find(
					config.geomask,
					";",
					0,
					true
				)
				and ";"
			)
			or ","
		local pattern =
			"%s*" .. separator .. "%s*"
		local maskItems =
			mw.text.split(
				mw.ustring.gsub(
					config.geomask,
					"SWITCH:",
					""
				),
				pattern
			)
		if #maskItems > 1 then
			-- No external labels are available,
			-- so use the supplied IDs as switch labels.
			args.switch =
				table.concat(
					maskItems,
					"###"
				)
			maskItem =
				"SWITCH:"
				.. table.concat(
					maskItems,
					","
				)
		elseif #maskItems == 1 then
			maskItem = maskItems[1]
		end
	end
	----------------------------------------------------------------------
	-- EXPLICIT GEOMASK
	--
	-- Property-based geomasks such as P131 are not supported because
	-- they require an entity database.
	----------------------------------------------------------------------
	if not maskItem and config.geomask then
		local geomask =
			mw.text.trim(
				config.geomask
			)
		if geomask ~= ''
			and not mw.ustring.match(
				geomask,
				"^[Pp]%d+$"
			) then
			maskItem = geomask
		end
	end
	----------------------------------------------------------------------
	-- ENSURE SOMETHING CAN BE DRAWN
	----------------------------------------------------------------------
	if not maskItem
		and not showShape
		and not showLine
		and not showMarker then
		return false, util.trackAndWarn(
			'Pages using infobox mapframe with no geometry',
			'No geometry specified for mapframe'
		)
	end
	----------------------------------------------------------------------
	-- ARGUMENT NUMBERING
	----------------------------------------------------------------------
	local argNumber = ''
	local function incrementArgNumber()
		if argNumber == '' then
			argNumber = 2
		else
			argNumber = argNumber + 1
		end
	end
	----------------------------------------------------------------------
	-- GEOMASK
	----------------------------------------------------------------------
	if maskItem then
		args["type" .. argNumber] =
			"shape-inverse"
		args["id" .. argNumber] =
			maskItem
		args["stroke-width" .. argNumber] =
			config["geomask-stroke-width"]
			or DEFAULT_GEOMASK_STROKE_WIDTH
		args["stroke-color" .. argNumber] =
			config["geomask-stroke-color"]
			or config["geomask-stroke-colour"]
			or DEFAULT_GEOMASK_STROKE_COLOR
		args["fill" .. argNumber] =
			config["geomask-fill"]
			or DEFAULT_GEOMASK_FILL
		args["fill-opacity" .. argNumber] =
			config["geomask-fill-opacity"]
			or DEFAULT_SHAPE_FILL_OPACITY
		-- Let Kartographer determine zoom and position
		-- unless explicitly configured.
		if not config.zoom
			and not config.switcher then
			args.zoom = nil
			args["frame-coord"] = nil
			args["frame-lat"] = nil
			args["frame-long"] = nil
		end
		incrementArgNumber()
		-- Hack retained from the original module.
		if not args.zoom then
			args["type" .. argNumber] =
				"line"
			args["id" .. argNumber] =
				maskItem
			args["stroke-width" .. argNumber] =
				0
			incrementArgNumber()
		end
	end
	----------------------------------------------------------------------
	-- SHAPE
	----------------------------------------------------------------------
	if showShape then
		args["type" .. argNumber] =
			shapeType
		-- Explicit ID only.
		if config.id then
			args["id" .. argNumber] =
				config.id
		elseif config.from then
			args["from" .. argNumber] =
				config.from
		end
		args["stroke-width" .. argNumber] =
			config["shape-stroke-width"]
			or config["stroke-width"]
			or DEFAULT_SHAPE_STROKE_WIDTH
		args["stroke-color" .. argNumber] =
			config["shape-stroke-color"]
			or config["shape-stroke-colour"]
			or config["stroke-color"]
			or config["stroke-colour"]
			or DEFAULT_SHAPE_STROKE_COLOR
		args["fill" .. argNumber] =
			config["shape-fill"]
			or DEFAULT_SHAPE_FILL
		args["fill-opacity" .. argNumber] =
			config["shape-fill-opacity"]
			or DEFAULT_SHAPE_FILL_OPACITY
		incrementArgNumber()
	end
	----------------------------------------------------------------------
	-- LINE
	----------------------------------------------------------------------
	if showLine then
		args["type" .. argNumber] =
			"line"
		-- Explicit ID only.
		if config.id then
			args["id" .. argNumber] =
				config.id
		elseif config.from then
			args["from" .. argNumber] =
				config.from
		end
		args["stroke-width" .. argNumber] =
			config["line-stroke-width"]
			or config["stroke-width"]
			or DEFAULT_LINE_STROKE_WIDTH
		args["stroke-color" .. argNumber] =
			config["line-stroke-color"]
			or config["line-stroke-colour"]
			or config["stroke-color"]
			or config["stroke-colour"]
			or DEFAULT_LINE_STROKE_COLOR
		incrementArgNumber()
	end
	----------------------------------------------------------------------
	-- POINT MARKER
	----------------------------------------------------------------------
	if showMarker then
		args["type" .. argNumber] =
			"point"
		if config.id then
			args["id" .. argNumber] =
				config.id
		end
		if config.coord then
			args["coord" .. argNumber] =
				config.coord
		end
		if config.marker then
			args["marker" .. argNumber] =
				config.marker
		end
		args["marker-color" .. argNumber] =
			config["marker-color"]
			or config["marker-colour"]
			or DEFAULT_MARKER_COLOR
		incrementArgNumber()
	end
	----------------------------------------------------------------------
	-- FRAME CENTERING
	----------------------------------------------------------------------
	if not maskItem
		and not showShape
		and not showLine
		and showMarker then
		if config.coord then
			args["frame-coord"] =
				args["frame-coord"]
				or config.coord
		end
	end
	----------------------------------------------------------------------
	-- Normalise coordinates to plain decimals so Module:Mapframe does
	-- not have to parse {{Coord}} HTML.
	----------------------------------------------------------------------
	local lat, lon = util.resolveCoordPair(
		args.coord
		or args['coord2']
		or args['frame-coord']
		or config.coord
		or config['frame-coord']
	)
	if lat and lon then
		local pair = string.format('%s, %s', lat, lon)
		args.coord = pair
		args['frame-coord'] = pair
		args['frame-lat'] = tostring(lat)
		args['frame-long'] = tostring(lon)
		args['frame-latitude'] = tostring(lat)
		args['frame-longitude'] = tostring(lon)
		-- Numbered point feature used by Mapframe content builder
		if args.type == 'point' then
			args.coord = pair
		end
		if args.type2 == 'point' then
			args.coord2 = pair
		end
	elseif not args['frame-lat'] or not args['frame-long'] then
		return false, util.trackAndWarn(
			'Pages using infobox mapframe with missing coordinates',
			'Mapframe could not parse coordinates'
		)
	end
	----------------------------------------------------------------------
	-- RENDER MAP
	----------------------------------------------------------------------
	local mapframe =
		args.switch
		and mf.multi(args)
		or mf._main(args)
	return true, mapframe .. tracking
end

----------------------------------------------------------------------
-- ENTRY POINTS
----------------------------------------------------------------------
p.main = function(frame)
	local parent =
		frame.getParent(frame)
	local parentArgs =
		parent.args
	local _, mapframe =
		p._main(parentArgs)
	return frame:preprocess(mapframe)
end

p.auto = function(frame)
	if not util.shouldAutoRun(frame) then
		return util.noop(
			"auto should not autorun"
		)
	end
	local args =
		util.argsFromAuto(frame)
	if args.custom then
		return frame:preprocess(
			util.parseCustomWikitext(
				args.custom
			)
		)
	end
	local _, mapframe =
		p._main(args)
	return frame:preprocess(mapframe)
end

p.autocaption = function(frame)
	if not util.shouldAutoRun(frame) then
		return util.noop(
			"autocaption should not autorun"
		)
	end
	local args =
		util.argsFromAuto(frame)
	local caption =
		p._caption(args)
	return caption
end

p.autoWithCaption = function(frame)
	if not util.shouldAutoRun(frame) then
		return util.noop(
			"autoWithCaption should not autorun"
		)
	end
	local args =
		util.argsFromAuto(frame)
	local wikitext
	local caption
	local ok
	if args.custom then
		ok = true
		wikitext =
			util.parseCustomWikitext(
				args.custom
			)
	else
		ok, wikitext =
			p._main(args)
	end
	if not ok then
		return wikitext
	end
	wikitext =
		frame:preprocess(
			wikitext
		)
	caption =
		p._caption(args)
	local data =
		mw.html.create()
			:wikitext(wikitext)
	data:tag('div')
		:addClass('infobox-caption')
		:cssText(args.captionstyle)
		:wikitext(caption)
	return tostring(data)
end

return p