Actions

Module

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

--------------------------------------------------
-- Module:IssueLink
-- Shared logic for {{last}}, {{next}}, {{between}}
--------------------------------------------------

local p = {}

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

local function trim(s)
	if not s then return "" end
	s = s:gsub("^%s+", "")
	s = s:gsub("%s+$", "")
	return s
end

local function splitOnce(str, sep)
	if not str then return nil, nil end
	local startPos, endPos = str:find(sep, 1, true)
	if not startPos then
		return nil, nil
	end
	local left = str:sub(1, startPos - 1)
	local right = str:sub(endPos + 1)
	return left, right
end

local function getBase(frame)
	local title = mw.title.getCurrentTitle()
	local text = title.text or ""

	-- Try: "<seriesPart> <issue>"
	local seriesPart, issueStr = text:match("^(.-)%s+(%d+)$")
	if not seriesPart then
		-- Fallback: treat entire title as series with no issue
		seriesPart = text
	end

	seriesPart = trim(seriesPart or "")

	local baseTitle = seriesPart
	local seriesSuffix = ""

	-- Split base title vs "Vol. X ..." suffix if present
	local t1, t2 = seriesPart:match("^(.-)%s+(Vol%.%s*%d+.*)$")
	if t1 then
		baseTitle = trim(t1)
		seriesSuffix = trim(t2)
	end

	return {
		seriesPart   = seriesPart,               -- e.g. "Green Lantern Vol. 4"
		baseTitle    = baseTitle,               -- e.g. "Green Lantern"
		seriesSuffix = seriesSuffix,            -- e.g. "Vol. 4"
		issue        = tonumber(issueStr) or nil
	}
end

local function unknownMarker()
	return '<font color="#cc0000">???</font>[[Category:Chronology incomplete]]'
end

local function parseExtra(extra, spec)
	extra = trim(extra or "")
	if extra == "" then return end

	-- Tokenize on ';' and ',' and whitespace.
	-- Also supports compact forms like "pg5pan2" in a single token.
	for token in extra:gmatch("[^;,%s]+") do
		token = trim(token)
		if token ~= "" then
			local t = token:lower()

			if t == "fb" then
				spec.flashback = true
			elseif t == "bts" then
				spec.behind = true
			end

			-- Extract page/panel even if combined (e.g., "pg5pan2")
			local pg  = token:match("pg(%d+)")
			local pan = token:match("pan(%d+)")
			if pg  then spec.page  = tonumber(pg)  end
			if pan then spec.panel = tonumber(pan) end
		end
	end
end

local function splitMainAndExtra(s)
	s = trim(s or "")
	if s == "" then return "", nil end

	-- First try the balanced-parens pattern
	local main, paren = s:match("^(.-)%s*(%b())%s*$")
	if paren then
		return trim(main), paren:sub(2, -2)
	end

	-- Fallback: if there's a '(' and the string ends with ')', split there
	local i = s:find("%(")
	if i and s:sub(-1) == ")" then
		return trim(s:sub(1, i - 1)), s:sub(i + 1, -2)
	end

	-- No extra block
	return s, nil
end

--------------------------------------------------
-- Parsing: same-title shorthand
--------------------------------------------------

local function parseSameTitleSpec(raw, base)
	local s = trim(raw)
	if s == "" then return nil end

	-- Strip optional "issue " and leading "#"
	s = s:gsub("^[Ii]ssue%s+", "", 1)
	s = s:gsub("^#", "", 1)
	s = trim(s)
	if s == "" then return nil end

	-- Peel a trailing "(...)" if present, capturing extras
	local extra = nil
	local paren = s:match("%b()%s*$")
	if paren then
		extra = paren:sub(2, -2)
		s = s:gsub("%s*%b()%s*$", "", 1)  -- drop the final "(...)"
		s = trim(s)
	end

	-- Accept "N" or "N/M"
	local issueStr, storyStr = s:match("^(%d+)%s*/%s*(%d+)$")
	local issue, story
	if issueStr then
		issue = tonumber(issueStr)
		story = tonumber(storyStr)
	else
		issueStr = s:match("^(%d+)$")
		if not issueStr then return nil end
		issue = tonumber(issueStr)
	end

	local spec = {
		seriesPart   = base.seriesPart,
		baseTitle    = base.baseTitle,
		seriesSuffix = base.seriesSuffix,
		issue        = issue,
		story        = story,
		isSameTitle  = true,
		isExternal   = false,
		flashback    = false,
		behind       = false,
		page         = nil,
		panel        = nil,
	}

	if extra and extra ~= "" then
		parseExtra(extra, spec)
	end

	return spec
end

--------------------------------------------------
-- Parsing: explicit/other-title specs
--------------------------------------------------

local function parseExternalSpec(raw)
	local s = trim(raw)
	if s == "" then return nil end

	-- Peel a trailing "(...)" if present, capturing extras
	local extra = nil
	local paren = s:match("%b()%s*$")
	if paren then
		extra = paren:sub(2, -2)
		s = s:gsub("%s*%b()%s*$", "", 1)
		s = trim(s)
	end

	-- From here on, 's' is the clean title/# part, and 'extra' holds fb/bts/pg/pan
	-- If it contains "#", treat as numbered issue
	if s:find("#", 1, true) then
		local beforeHash, hashPart = s:match("^(.-)%s*#(.-)$")
		if not beforeHash or beforeHash == "" or not hashPart or hashPart == "" then
			return nil
		end

		local seriesPart = trim(beforeHash)
		hashPart = trim(hashPart)

		local issueStr, storyStr = hashPart:match("^(%d+)%s*/%s*(%d+)$")
		local issue, story
		if issueStr then
			issue = tonumber(issueStr)
			story = tonumber(storyStr)
		else
			issueStr = hashPart:match("^(%d+)$")
			if not issueStr then return nil end
			issue = tonumber(issueStr)
		end

		local baseTitle = seriesPart
		local suffix = ""
		local t1, t2 = seriesPart:match("^(.-)%s+(Vol%.%s*%d+.*)$")
		if t1 then
			baseTitle = trim(t1)
			suffix = trim(t2)
		end

		local spec = {
			seriesPart   = seriesPart,
			baseTitle    = baseTitle,
			seriesSuffix = suffix,
			issue        = issue,
			story        = story,
			isSameTitle  = false,
			isExternal   = true,
			flashback    = false,
			behind       = false,
			page         = nil,
			panel        = nil,
		}

		if extra and extra ~= "" then
			parseExtra(extra, spec)
		end

		return spec
	else
		-- GN / unnumbered
		local title = s
		local baseTitle = title
		local suffix = ""
		local t1, t2 = title:match("^(.-)%s+(Vol%.%s*%d+.*)$")
		if t1 then
			baseTitle = trim(t1)
			suffix = trim(t2)
		end

		local spec = {
			seriesPart   = title,
			fullTitle    = title,
			baseTitle    = baseTitle,
			seriesSuffix = suffix,
			issue        = nil,
			story        = nil,
			isSameTitle  = false,
			isExternal   = true,
			flashback    = false,
			behind       = false,
			page         = nil,
			panel        = nil,
		}

		if extra and extra ~= "" then
			parseExtra(extra, spec)
		end

		return spec
	end
end

--------------------------------------------------
-- parseSpec: decides same-title vs explicit other-title
--------------------------------------------------

local function parseSpec(raw, base)
	local s = trim(raw)
	if s == "" then return nil end

	-- Special "???" marker
	if s == "???" then
		return {
			isUnknown = true
		}
	end

	-- Same-title shorthand: starts with "issue ", "#", or digit
	if s:match("^[Ii]ssue%s+%d") or s:match("^#%d") or s:match("^%d") then
		return parseSameTitleSpec(s, base)
	end

	-- Otherwise, treat as explicit series name (may be numbered or GN)
	return parseExternalSpec(s)
end

local function parseBetweenRightSpec(rightRaw, leftSpec, base)
	rightRaw = trim(rightRaw or "")
	if rightRaw == "" then
		return nil
	end

	-- "issue 12" -> always current series
	if rightRaw:match("^[Ii]ssue%s+") then
		return parseSpec(rightRaw, base)

	-- "#12" with explicit left series -> reuse left's series
	elseif rightRaw:match("^#%d") and leftSpec.isExternal and leftSpec.seriesPart and leftSpec.seriesPart ~= "" then
		local newRight = leftSpec.seriesPart .. " " .. rightRaw
		return parseSpec(newRight, base)

	-- "7/3(pg...)" with explicit left series -> reuse left's series
	elseif rightRaw:match("^%d") and leftSpec.isExternal and leftSpec.seriesPart and leftSpec.seriesPart ~= "" then
		local newRight = leftSpec.seriesPart .. " #" .. rightRaw
		return parseSpec(newRight, base)

	else
		return parseSpec(rightRaw, base)
	end
end

local function parseSingleBetweenSpec(raw, base)
	raw = trim(raw or "")
	local sep = " and "
	local searchFrom = 1

	-- First try an issue-aware split.
	-- This avoids splitting inside titles such as "Brave and the Bold".
	while true do
		local startPos, endPos = raw:find(sep, searchFrom, true)
		if not startPos then
			break
		end

		local leftRaw = trim(raw:sub(1, startPos - 1))
		local rightRaw = trim(raw:sub(endPos + 1))

		local leftSpec = parseSpec(leftRaw, base)
		if leftSpec and leftSpec.issue then
			local rightSpec = parseBetweenRightSpec(rightRaw, leftSpec, base)
			if rightSpec then
				return leftSpec, rightSpec
			end
		end

		searchFrom = endPos + 1
	end

	-- Fallback: preserve the old behavior for non-numbered / GN cases.
	local leftRaw, rightRaw = splitOnce(raw, sep)
	if not rightRaw then
		return nil, nil
	end

	local leftSpec = parseSpec(leftRaw, base)
	if not leftSpec then
		return nil, nil
	end

	local rightSpec = parseBetweenRightSpec(rightRaw, leftSpec, base)
	if not rightSpec then
		return nil, nil
	end

	return leftSpec, rightSpec
end

--------------------------------------------------
-- Link + location rendering
--------------------------------------------------

local function buildLocation(spec)
	local parts = {}

	if spec.page then
		table.insert(parts, "page " .. spec.page)
	end
	if spec.panel then
		table.insert(parts, "panel " .. spec.panel)
	end

	if #parts == 0 then
		return ""
	else
		return ", " .. table.concat(parts, ", ")
	end
end

-- Build page title for a numbered issue, applying the Vol. 1 rule:
-- "Superman Vol. 1 #4" -> page "Superman 4"
-- "Superman Vol. 3 #4" -> page "Superman Vol. 3 4"
local function buildPageTitle(spec)
	local seriesPart = trim(spec.seriesPart or "")
	if seriesPart == "" or not spec.issue then
		return nil
	end

	if spec.isExternal and spec.seriesSuffix == "Vol. 1" and spec.baseTitle then
		-- Drop "Vol. 1" from the page title, keep baseTitle only
		return trim(spec.baseTitle) .. " " .. spec.issue
	else
		return seriesPart .. " " .. spec.issue
	end
end

local function makeLink(spec)
	-- Unknown marker
	if spec.isUnknown then
		return unknownMarker()
	end

	-- Numbered issues
	if spec.issue then
		local page = buildPageTitle(spec)
		if not page or page == "" then return "" end
		if spec.story then
			page = page .. "#" .. spec.story
		end

		local display
		if spec.isSameTitle then
			-- Same-title: just "#5", "#5/2"
			display = "#" .. spec.issue
			if spec.story then
				display = display .. "/" .. spec.story
			end
		elseif spec.isExternal then
			-- Other title: ''Series'' Vol. X #N
			local titleText = "''" .. (spec.baseTitle or spec.seriesPart or "") .. "''"
			if spec.seriesSuffix and spec.seriesSuffix ~= "" then
				titleText = titleText .. " " .. spec.seriesSuffix
			end
			local numText = "#" .. spec.issue
			if spec.story then
				numText = numText .. "/" .. spec.story
			end
			display = titleText .. " " .. numText
		else
			-- Fallback
			display = "#" .. spec.issue
			if spec.story then
				display = display .. "/" .. spec.story
			end
		end

		return "[[" .. page .. "|" .. display .. "]]"
	end

	-- GN / unnumbered
	local title = spec.fullTitle or spec.seriesPart or ""
	title = trim(title)
	if title == "" then return "" end

	local titleText = "''" .. (spec.baseTitle or title) .. "''"
	if spec.seriesSuffix and spec.seriesSuffix ~= "" then
		titleText = titleText .. " " .. spec.seriesSuffix
	end

	return "[[" .. title .. "|" .. titleText .. "]]"
end

--------------------------------------------------
-- last / next shared helper
--------------------------------------------------

local function buildRelative(frame, isNext)
	local parent = frame:getParent()
	local args = parent and parent.args or frame.args
	local raw  = trim(args[1] or "")
	local role = trim(args.role or args["role"] or "")

	-- Special "???" marker: last/next in unknown
	if raw == "???" then
		local word = isNext and "next" or "last"
		local phrase = word
		if role ~= "" then
			phrase = phrase .. " as " .. role
		end
		return phrase .. " in " .. unknownMarker()
	end

	local base = getBase(frame)
	local spec

	if raw == "" then
		-- Default: previous/next issue of same title
		if not base.issue then
			return ""
		end
		local delta = isNext and 1 or -1
		local targetIssue = base.issue + delta
		if targetIssue < 1 then
			return ""
		end
		spec = {
			seriesPart   = base.seriesPart,
			baseTitle    = base.baseTitle,
			seriesSuffix = base.seriesSuffix,
			issue        = targetIssue,
			story        = nil,
			isSameTitle  = true,
			isExternal   = false,
			flashback    = false,
			behind       = false,
			page         = nil,
			panel        = nil,
		}
	else
		spec = parseSpec(raw, base)
	end

	if not spec then return "" end

	local link = makeLink(spec)
	if link == "" then return "" end

	local loc = buildLocation(spec)
	local word = isNext and "next" or "last"

	-- Build the prefix: "last", "last behind the scenes", "last behind the scenes as Robin"
	local phrase = word
	if spec.behind then
		phrase = phrase .. ", behind the scenes,"
	end
	if role ~= "" then
		phrase = phrase .. " as " .. role
	end

	-- Build the tail: " in issue ...", " in flashback in issue ...", etc.
	local tail
	if spec.flashback then
		-- Same-title numbered issue → "in flashback in issue ..."
		if spec.issue and spec.isSameTitle then
			tail = " in flashback in issue " .. link .. loc
		else
			-- External or GN → no "issue"
			tail = " in flashback in " .. link .. loc
		end
	else
		-- Same-title numbered issue → "in issue ..."
		if spec.issue and spec.isSameTitle then
			tail = " in issue " .. link .. loc
		else
			-- External or GN → no "issue"
			tail = " in " .. link .. loc
		end
	end

	return phrase .. tail
end

function p.last(frame)
	return buildRelative(frame, false)
end

function p.next(frame)
	return buildRelative(frame, true)
end

--------------------------------------------------
-- between
--------------------------------------------------

function p.between(frame)
	local parent = frame:getParent()
	local args = parent and parent.args or frame.args

	local base = getBase(frame)
	local raw1 = trim(args[1] or "")
	local raw2 = trim(args[2] or "")

	local role = trim(args.role or args["role"] or "")

	-- Global flashback flag for the whole "between" relation.
	-- "fb" can be either the second unnamed param (for single-parameter "X and Y")
	-- or the third unnamed param (for two-parameter "|left|right|fb").
	local betweenFb = false

	if raw2:lower() == "fb" then
		betweenFb = true
		raw2 = ""  -- treat as no second spec parameter
	end

	local extra2 = trim(args[3] or "")
	if extra2 ~= "" and extra2:lower() == "fb" then
		betweenFb = true
	end

	local leftSpec, rightSpec

	--------------------------------------------------
	-- Special case: single-parameter panel-range syntax:
	-- e.g. "Superman Vol. 3 #5(pg6,pan5&pan6)"
	--      "Superman Vol. 3 #5(pg6,pan5&pg7,pan2)"
	--      "Superman Vol. 3 #5(fb;pg6,pan5&pan6)"
	--------------------------------------------------
	if raw1 ~= "" and raw2 == "" then
		local main, paren = raw1:match("^(.-)%s*(%b())%s*$")
		if paren and paren:find("&", 1, true) then
			main = trim(main or "")
			local content = paren:sub(2, -2)  -- strip parentheses

			-- Optional "fb;" prefix inside the parentheses
			local fb = false
			local fbPart, rest = content:match("^(fb);(.*)$")
			if fbPart then
				fb = true
				content = trim(rest or "")
			end

			-- Split two locations on "&"
			local leftLocStr, rightLocStr = content:match("^(.-)&(.-)$")
			if leftLocStr and rightLocStr then
				-- Parse the base issue spec (same-title or external)
				local baseSpec = parseSpec(main, base)
				if not baseSpec then
					return ""
				end

				if fb then
					baseSpec.flashback = true
				end

				-- Parse a single "pgX,panY" / "pgX" / "panY" location,
				-- optionally inheriting a default page (for the second half).
				local function parseLoc(loc, defaultPage)
					loc = trim(loc or "")
					local pg = loc:match("pg(%d+)")
					local pan = loc:match("pan(%d+)")
					if not pg and defaultPage then
						pg = defaultPage
					end
					return pg and tonumber(pg) or nil, pan and tonumber(pan) or nil
				end

				local pg1, pan1 = parseLoc(leftLocStr, nil)
				local pg2, pan2 = parseLoc(rightLocStr, pg1)

				local link = makeLink(baseSpec)

				-- Build "page / panel" range text for the tail
				local function buildRangeText()
					
					-- Special nice case: two pages, no panels -> "pages 6 and 8"
					if pg1 and pg2 and not pan1 and not pan2 then
						return "pages " .. pg1 .. " and " .. pg2
					end
					
					-- Special nice case: same page and two panels -> "page 6, panels 5 and 6"
					if pg1 and pg2 and pg1 == pg2 and pan1 and pan2 then
						return "page " .. pg1 .. ", panels " .. pan1 .. " and " .. pan2
					end

					local segs = {}

					if pg1 or pan1 then
						local seg = ""
						if pg1 then
							seg = "page " .. pg1
						end
						if pan1 then
							if seg ~= "" then
								seg = seg .. ", panel " .. pan1
							else
								seg = "panel " .. pan1
							end
						end
						table.insert(segs, seg)
					end

					if pg2 or pan2 then
						local seg = ""
						if pg2 then
							seg = "page " .. pg2
						end
						if pan2 then
							if seg ~= "" then
								seg = seg .. ", panel " .. pan2
							else
								seg = "panel " .. pan2
							end
						end
						table.insert(segs, seg)
					end

					if #segs == 0 then
						return ""
					elseif #segs == 1 then
						return segs[1]
					else
						return segs[1] .. " and " .. segs[2]
					end
				end

				local rangeText = buildRangeText()

				-- Prefix: flashback + same-title vs external
				local prefix
				if baseSpec.flashback then
					if baseSpec.isSameTitle and baseSpec.issue then
						prefix = "in between flashback in issue " .. link
					else
						prefix = "in between flashback in " .. link
					end
				else
					if baseSpec.isSameTitle and baseSpec.issue then
						prefix = "in between issue " .. link
					else
						prefix = "in between " .. link
					end
				end

				local text
				if rangeText ~= "" then
					text = prefix .. ", " .. rangeText
				else
					text = prefix
				end

				-- Apply global flashback + role
				if betweenFb then
					text = "in flashback " .. text
				end
				if role ~= "" then
					text = "as " .. role .. " " .. text
				end

				return text
			end
			-- If we get here, we had parentheses with "&" but couldn't parse:
			-- fall through to normal logic as a fallback.
		end
	end

	--------------------------------------------------
	-- Normal between logic
	--------------------------------------------------

	if raw1 == "" and raw2 == "" then
		-- Default: N-1 and N+1 of same title
		if not base.issue then
			return ""
		end
		local leftIssue = base.issue - 1
		local rightIssue = base.issue + 1
		if leftIssue < 1 then
			leftIssue = nil
		end
		if not leftIssue and not rightIssue then
			return ""
		end

		if leftIssue then
			leftSpec = {
				seriesPart   = base.seriesPart,
				baseTitle    = base.baseTitle,
				seriesSuffix = base.seriesSuffix,
				issue        = leftIssue,
				story        = nil,
				isSameTitle  = true,
				isExternal   = false,
				flashback    = false,
				page         = nil,
				panel        = nil,
			}
		end
		if rightIssue then
			rightSpec = {
				seriesPart   = base.seriesPart,
				baseTitle    = base.baseTitle,
				seriesSuffix = base.seriesSuffix,
				issue        = rightIssue,
				story        = nil,
				isSameTitle  = true,
				isExternal   = false,
				flashback    = false,
				page         = nil,
				panel        = nil,
			}
		end

	elseif raw1 ~= "" and raw2 == "" then
		-- Single parameter, expect "X and Y"
		-- Uses an issue-aware split so titles containing "and" still work.
		leftSpec, rightSpec = parseSingleBetweenSpec(raw1, base)
		if not leftSpec or not rightSpec then
			return ""
		end
	else
		-- Two explicit parameters
		if raw1 ~= "" then
			leftSpec = parseSpec(raw1, base)
		end
		if raw2 ~= "" then
			rightSpec = parseSpec(raw2, base)
		end
	end

	if not leftSpec or not rightSpec then
		return ""
	end

	-- Same external series? (e.g. "Teen Titans Vol. 3 #33 and #34")
	local sameExternalSeries =
		leftSpec.isExternal and rightSpec.isExternal and
		leftSpec.seriesPart and rightSpec.seriesPart and
		leftSpec.seriesPart == rightSpec.seriesPart

	-- Build left link normally
	local linkL = makeLink(leftSpec)

	-- Build right link; if same external series, drop title in display
	local linkR
	if sameExternalSeries and rightSpec.issue then
		local page = buildPageTitle(rightSpec)
		if page and page ~= "" then
			if rightSpec.story then
				page = page .. "#" .. rightSpec.story
			end
			local display = "#" .. rightSpec.issue
			if rightSpec.story then
				display = display .. "/" .. rightSpec.story
			end
			linkR = "[[" .. page .. "|" .. display .. "]]"
		else
			linkR = makeLink(rightSpec)
		end
	else
		linkR = makeLink(rightSpec)
	end

	if linkL == "" or linkR == "" then
		return ""
	end

	local locL = buildLocation(leftSpec)
	local locR = buildLocation(rightSpec)

	local leftFb  = leftSpec.flashback and true or false
	local rightFb = rightSpec.flashback and true or false

	local bothSameTitle = leftSpec.isSameTitle and rightSpec.isSameTitle

	-- Is one of the sides the "main story" issue (current page), for flashback-between?
	local currentIssue = base.issue
	local leftIsCurrent  = currentIssue and leftSpec.isSameTitle  and leftSpec.issue  == currentIssue or false
	local rightIsCurrent = currentIssue and rightSpec.isSameTitle and rightSpec.issue == currentIssue or false

	local text

	-- Helper to describe one side (left or right)
	local function describeSide(spec, link, loc)
		-- Unknown marker: just return "???", no "issue"/"flashback" wording
		if spec.isUnknown then
			return link .. loc  -- link is already the colored "???" marker
		end

		-- Main story case: same issue as the current page, non-flashback, and overall "between" is flashback
		if betweenFb and currentIssue and spec.isSameTitle and spec.issue == currentIssue and not spec.flashback then
			return "main story"
		end

		local seg
		if spec.flashback then
			-- Flashback side
			seg = "flashback in "
			if spec.isSameTitle and spec.issue then
				seg = seg .. "issue "
			end
			seg = seg .. link .. loc
		else
			-- Normal (non-flashback) side
			if spec.isSameTitle and spec.issue then
				seg = "issue " .. link .. loc
			else
				seg = link .. loc
			end
		end
		return seg
	end

	-- Special case: both non-flashback, same-title numbered issues
	-- AND no page/panel info on either side
	-- AND not a flashback-between involving the current issue as "main story"
	-- → "in between issues [#x] and [#y]"
	if not leftFb
		and not rightFb
		and bothSameTitle
		and leftSpec.issue
		and rightSpec.issue
		and not leftSpec.page
		and not leftSpec.panel
		and not rightSpec.page
		and not rightSpec.panel
		and not (betweenFb and (leftIsCurrent or rightIsCurrent))
	then
		text = "in between issues " .. linkL .. locL .. " and " .. linkR .. locR
	else
		-- All other combinations (cross-title, GN, any flashback,
		-- or when page/panel info is present, or main story involvement)
		local descL = describeSide(leftSpec, linkL, locL)
		local descR = describeSide(rightSpec, linkR, locR)
		text = "in between " .. descL .. " and " .. descR
	end

	-- Wrap with global flashback + role
	if betweenFb then
		text = "in flashback " .. text
	end
	if role ~= "" then
		text = "as " .. role .. " " .. text
	end

	return text
end

return p