The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

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