Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Please sign up or log in to edit the wiki.
local mArguments -- initialize lazily
local mCommon = require( 'Module:Common' )
local floatingui = require( 'Module:FloatingUI' )
local data = mw.loadJsonData( 'Module:Cite DTP/data.json' )
local p = {}

-- Detect and strip a trailing -fn marker for parsing,
-- while returning a flag to allow special display / URL behaviour.
local function parseFootnoteSuffix( dtpId )
	if not dtpId then
		return dtpId, false
	end

	dtpId = mw.text.trim( dtpId )

	if mw.ustring.match( dtpId, '%-fn$' ) then
		return mw.ustring.gsub( dtpId, '%-fn$', '' ), true
	end

	return dtpId, false
end

--- Split DTP ID into a table of string
---
--- @param dtpId string - ID used for DTP
--- @return table
local function splitDTP( dtpId )
	local temp = mCommon.splitStringIntoTable( dtpId, ' ' )
	if not temp[2] then
		return temp
	end
	local parts = mCommon.splitStringIntoTable( temp[2], '.' )
	table.insert( parts, 1, temp[1] )
	mw.logObject( parts, '[Cite DTP] DTP is segemented into' )
	return parts
end

--- Return URL to Cite Tolkien
---
--- @param dtpParts table - Table of DTP ID parts
--- @return string
local function getDTPUrl( dtpParts )
	local path = string.format(
		'/%s/%s',
		dtpParts[1],
		string.sub( table.concat( dtpParts, '.' ), #dtpParts[1] + 2 )
	)
	-- Always include trailing slash so suffix logic is reliable
	return 'https://cite.digitaltolkien.com' .. path .. '/'
end

--- Get the matching type table from the DTP part
---
--- @param part string - The current part of the DTP
--- @param schema table - The schema field of the current part
--- @return table|nil
local function getTypeData( part, schema )
	if type( schema ) == 'table' then
		for _, v in ipairs( schema ) do
			local currType = data['types'][v]
			local typePattern = '^' .. currType['pattern'] .. '$'
			if string.match( part, typePattern ) then
				local copy = {}
				for j, x in pairs( currType ) do copy[j] = x end
				copy['key'] = v
				return copy
			end
		end
		error( string.format( 'No matching type found for %s', part ) )
	end

	local matchedType = data['types'][schema]
	if not matchedType then
		error( string.format( 'No matching type definition for %s', schema ) )
	end

	local pattern = '^' .. matchedType['pattern'] .. '$'
	if string.match( part, pattern ) then
		local copy = {}
		for j, x in pairs( matchedType ) do copy[j] = x end
		copy['key'] = schema
		return copy
	end
end

--- Build a table of citation info from the DTP ID
---
--- @param dtpParts table - Table of DTP ID parts
--- @return table|nil
function p.buildCiteData( dtpParts )
	local cite = {}

	local matchedWork
	local n = 1 -- schema index (schema are ordered by i - 1)

	for i, part in pairs( dtpParts ) do
		if i == 1 then
			matchedWork = mCommon.tableContainsKey( data['works'], part )
			if not matchedWork then
				error( string.format( 'No matching work for %s', part ) )
			elseif not matchedWork['author'] then
				error( string.format( 'Missing author for %s', part ) )
			elseif not matchedWork['label'] then
				error( string.format( 'Missing label for %s', part ) )
			elseif not matchedWork['schema'] then
				error( string.format( 'Missing schema for %s', part ) )
			end

			local authors = {}
			for _, author in pairs( matchedWork['author'] ) do
				table.insert( authors, string.format( '[[%s]]', author ) )
			end

			cite['author'] = table.concat( authors, ', ' )
			cite['work'] = string.format( '[[%s]]', matchedWork['label'] )
			cite['order'] = { 'author', 'work' }
		else
			if not matchedWork['schema'][n] then
				error( string.format( 'Missing schema #%s for %s', n, matchedWork['label'] ) )
			end

			local matchedType = getTypeData( part, matchedWork['schema'][n] )
			if not matchedType then
				error( string.format( '%s is not a valid input for field %d', part, n ) )
			end

			local typeKey = matchedType['key']
			local key = part
			local numKey = tonumber( part ) -- strips leading zeros

			-- Default label from type (may be blank for paragraph in data.json)
			local label = string.gsub( matchedType['label'] or '', '%$1', numKey or key )

			-- SPECIAL CASE: paragraphs must render as "Paragraph N"
			if typeKey == 'paragraph' then
				label = string.format( 'Paragraph %s', tostring( numKey or key ) )
			end

			-- Custom labels from work data (chapters/books/etc.)
			if matchedWork[typeKey] then
				local match
				local partKey = dtpParts[n]
				if tonumber( dtpParts[n] ) then
					partKey = tonumber( dtpParts[n] )
				end

				if type( matchedWork[typeKey][numKey or key] ) == 'string' then
					match = matchedWork[typeKey][numKey or key]
				elseif
					matchedWork[typeKey][partKey] and
					matchedWork[typeKey][partKey][numKey or key] and
					type( matchedWork[typeKey][partKey][numKey or key] ) == 'string'
				then
					match = matchedWork[typeKey][partKey][numKey or key]
				end

				if match then
					label = match
					label = string.format( '"[[%s]]"', label )
				end
			end

			-- If label is empty (and not a paragraph, which we force), do not add it
			if label and mw.text.trim( label ) ~= '' then
				cite[typeKey] = label
				table.insert( cite['order'], typeKey )
			end

			n = n + 1
		end
	end

	mw.logObject( cite, string.format( '[Cite DTP] Citation data of [%s]', table.concat( dtpParts, '.' ) ) )
	return cite
end

--- Get the HTML for the DTP identifier in the citation
---
--- @param dtpUrl string - URL to the DTP site
--- @param dtpId string - DTP ID
--- @return mw.html
local function getIdentifierHTML( dtpUrl, dtpId )
	local html = mw.html.create( 'span' )
		:addClass( 'cite-dtp-identifier' )
		:wikitext( string.format( '[%s %s]', dtpUrl, dtpId ) )

	local tooltipHtml = mw.html.create( 'span' )
		:addClass( 'cite-dtp-identifier-tooltip' )
		:wikitext( string.format( '[%s %s]',
			'https://digitaltolkien.com/citation-systems',
			'Digital Tolkien Project Citation Systems'
		) )

	return floatingui.render( {
		reference = html,
		content = tooltipHtml
	} )
end

-- Only insert a cite part if it has visible content
local function insertCitePart( cite, key, value )
	if not value then
		return
	end
	local trimmed = mw.text.trim( tostring( value ) )
	if trimmed == '' then
		return
	end

	local spanHtml = mw.html.create( 'span' )
	spanHtml:addClass( 'cite-dtp-' .. key )
	spanHtml:attr( 'title', key )
	spanHtml:wikitext( value )
	table.insert( cite, tostring( spanHtml ) )
end

-- Implements {{Cite DTP}} from the frame
function p.citeDTP( frame )
	mArguments = require( 'Module:Arguments' )
	return p._citeDTP( mArguments.getArgs( frame ), frame )
end

function p._citeDTP( args, frame )
	if not args then
		return 'Missing arguments'
	end

	local dtpIdRaw = args['id'] or args[1]
	local dtpIdBase, isFootnote = parseFootnoteSuffix( dtpIdRaw )

	local dtpParts = splitDTP( dtpIdBase )
	local dtpUrl = getDTPUrl( dtpParts )

	-- If -fn used, make the link include it: .../LR/6.08.219-fn/
	if isFootnote then
		dtpUrl = mw.ustring.gsub( dtpUrl, '/$', '-fn/' )
	end

	local cite = {}
	local citeData = p.buildCiteData( dtpParts )

	for _, key in ipairs( citeData['order'] ) do
		insertCitePart( cite, key, citeData[key] )
	end

	-- Append "footnote" at the end (after paragraph etc.)
	if isFootnote then
		local spanHtml = mw.html.create( 'span' )
		spanHtml:addClass( 'cite-dtp-footnote' )
		spanHtml:attr( 'title', 'footnote' )
		spanHtml:wikitext( 'footnote' )
		table.insert( cite, tostring( spanHtml ) )
	end

	local wikitext = table.concat( cite, ', ' )
	local html = mw.html.create( 'cite' ):addClass( 'citation cite-dtp' )
	html:node( getIdentifierHTML( dtpUrl, dtpIdRaw ) )
	html:wikitext( wikitext )

	return table.concat( {
		frame:extensionTag {
			name = 'templatestyles', args = { src = 'Module:Cite DTP/styles.css' }
		},
		floatingui.load(),
		tostring( html )
	} )
end

return p