Module:DPLlua

Module documentation
This documentation is transcluded from Module:DPLlua/doc. [edit] [history] [purge]
Module:DPLlua requires Module:LibraryUtil.
Module:DPLlua is required by Module:DependencyList.

Uses Module:DPLlua/Helper to make it possible to include all parameters of a template while maintaining good performance.

This module is a helper module to be used by other modules; it may not be designed to be invoked directly. See Near-Reality:Lua/Helper modules for a full list and more information.

ModuleFunctionTypeUse
DPLluaask( ... )tablesask takes a series of tables each containing the settings for a DPL query; it will return the same number of result tables as input tables. All formatting settings are stripped from the config. If the config does not contains include, the result will be a simple list of page names.
{
	<pagename#1>,
	<pagename#2>,
	<pagename#3>,
}

A query with an include of the form include = '{template#1}:1:2:param1:param2, {template#2}:3:param1, %0' will give a result like

{
	['include'] = {
		['template#1'] = {
			[1] = val#1,
			[2] = val#2,
			['param1'] = val#3,
			['param2'] = val#4,
		},
		['template#2'] = {
			[3] = val#5,
			['param1'] = val#6,
		},
		['%0'] = val#7
	},
	['title'] = <pagename>
}

You can also do include = '{some template}' which will include all parameters of that template in their unexpanded form (templates are not expanded but some content in parser tags is placed in strip markers).

If the config value count is larger than 500 it will automatically generate multiple DPL queries with offsets and their outputs will be combined in the returned result.

The output contains a time field so you don't need to use %DPLTIME%.

If the DPL throws an error it will be available in the error field of the output.

Differences with normal DPL:

  • All formatting options are ignored
  • Surrogate templates (e.g. include = '{template|other template}' are ignored
  • Using include = '{template}' will include all the arguments of that template instead of expanding the template
  • The parameter count can go higher than 500
  • When the value of a parameter is a table it will be expanded into multiple lines. E.g. doing notcategory = {val#1, val#2} will expand into
|notcategory = val#1
|notcategory = val#2
Note of warning, if you include content containing § symbols the result may be unreliable. If you include a whole template (e.g. include = '{some template}'), content inside strip markers (not nowiki) can't be cleaned up inside lua so pipe characters (|) will be replaced with § characters and the { and } characters are replaced by (U+2774) and (U+2775). Use include = '{some template}, {some template}:1:2:3' instead for the problem parameters.

Example:

local dpl = require( 'Module:DPLlua' )

local a, b = dpl.ask( {
		namespace = '',
		linksto = 'Treasure trails',
		distinct = 'strict',
		ordermethod = 'title',
		count = 5,
		ignorecase = true
	},{
		namespace = '',
		uses = 'Template:Recipe',
		count = 1,
		include = '{Recipe},{Infobox Item}',
		ignorecase = true
	} )
mw.logObject(a)
mw.logObject(b)
--[=[
table#1 {
  "(beginner)",
  "(easy)",
  "(elite)",
  "(g)",
  "(hard)",
  ["time"] = 0.0541,
}
table#1 {
  table#2 {
    ["include"] = table#3 {
      ["Infobox Item"] = table#4 {
        ["destroy"] = "Drop",
        ["equipable"] = "No",
        ["examine"] = "It's a bar of 'perfect' gold.",
        ["id"] = "2365",
        ["image"] = "[[File:'perfect' gold bar.png]]",
        ["members"] = "Yes",
        ["name"] = "'perfect' gold bar",
        ["noteable"] = "No",
        ["placeholder"] = "Yes",
        ["quest"] = "[[Family Crest]]",
        ["release"] = "[[9 April]] [[2002]]",
        ["stackable"] = "No",
        ["tradeable"] = "No",
        ["update"] = "New members quest online!",
        ["value"] = "300",
        ["weight"] = "1.814",
      },
      ["Recipe"] = table#5 {
        ["facilities"] = "Furnace",
        ["mat1"] = "'perfect' gold ore",
        ["mat1cost"] = "No",
        ["members"] = "Yes",
        ["notes"] = "Partial completion of [[Family Crest]]",
        ["outputcost"] = "No",
        ["outputname"] = "'perfect' gold bar",
        ["skill1"] = "Smithing",
        ["skill1exp"] = "22.5",
        ["skill1lvl"] = "40",
        ["ticks"] = "4",
      },
    },
    ["title"] = "'perfect' gold bar",
  },
  ["time"] = 0.0541,
}
]=]

-- <nowiki>
local dpl = {}
local libraryUtil = require( 'libraryUtil' )
local checkType = libraryUtil.checkType
local checkTypeForNamedArg = libraryUtil.checkTypeForNamedArg

dpl.pipe = '¦'
local dataContentMarker = '`#@@#`'
local allIncludedParamNames = {}

-- Custom function for splitting a string because mw.text.split() is waaay too slow
local function split( str, pattern, plain )
	local res = {}
	local continue = true
	local startIndex = 1

	while continue do
		local i, j = string.find( str, pattern, startIndex, plain )
		if i then
			table.insert( res, string.sub( str, startIndex, i-1 ) )
			startIndex = j + 1
		else
			table.insert( res, string.sub( str, startIndex ) )
			continue = false
		end
	end

	return res
end

-- Also custom function for speed
local function trim( str )
	return string.match( str, '^%s*(.-)%s*$' )
end

local escapeChars = {
	['{'] = '&#123;',
	['❴'] = '&#123;', -- Wtf dpl...
	['}'] = '&#125;',
	['❵'] = '&#125;',
	['['] = '&#91;',
	[']'] = '&#93;',
	['|'] = '&#124;',
	['-'] = '&#8208;'
}
local function escape( str )
	-- the \226\157\180\181 are used to match ❴ (U+2774) and ❵ (U+2775) wich are 3 bytes long (UTF-8) so
	-- we can't use them directly inside [] patterns. Ustring would fix this but it's way too slow.
	str = string.gsub( str, '[{}%[%]|%-]', escapeChars ):gsub( '\226\157[\180\181]', escapeChars )
	return str
end

local unEscapeChars = {
	['&#123;'] = '{',
	['&#125;'] = '}',
	['&#91;'] = '[',
	['&#93;'] = ']',
	['&#124;'] = '|',
	['&#8208;'] = '-'
}
local function unEscape( str )
	str = string.gsub( str, '&#%d+;', unEscapeChars )
	return str
end

local function removeFormattingSettings( query )
	local toRemove = {
		'mode',
		'table',
		'tablerow',
		'tablesortcol',
		'headingmode',
		'headingcount',
		'listattr',
		'itemattr',
		'hlistattr',
		'hitemattr',
		'userdateformat',
		'shownamespace',
		'escapelinks',
		'titlemaxlength',
		'replaceintitle',
		'columns',
		'rows',
		'rowsize',
		'rowcolformat',
		'resultsheader',
		'resultsfooter',
		'oneresultheader',
		'oneresultfooter',
		'noresultsheader',
		'suppresserrors',
		'noresultsfooter',
		'format'
	}

	for _, k in ipairs( toRemove ) do
		query[k] = nil
	end
end

local function formatInclude( query )
	checkTypeForNamedArg( 'include', 'Module:DPLlua.ask', query, 'string' )
	query = split( query, ',', true )
	local includedParamNames = {}

	for i = 1, #query do
		if query[i]:match( '%b{}' ) then -- Check if we are including a template
			local templateName, params = query[i]:match( '{(.-)[¦|}]([^,]*)' )
			if params:find( '%S' ) then
				params:gsub( '^:%-', '' )
				query[i] = string.format( '{%s}%s', templateName, params )

				for param in params:gmatch( ':([^:]*)' ) do
					param = trim( param )
					table.insert( includedParamNames, { name=templateName, isTemplate=true, param=param } )
				end

			else
				query[i] = string.format( '{%s¦DPLlua helper}', templateName ) -- Use a helper template to get all the parameters of our included template
				table.insert( includedParamNames, { name=templateName, isTemplate=true, includeAll=true } )
			end
		else
			table.insert( includedParamNames, { name=trim( query[i] ) } )
		end
	end

	return table.concat( query, ',' ), includedParamNames
end

local function formatDpl( query )
	local queries = {}
	local count = query.count or 500
	local offset = query.offset or 0
	local usesInclude = false
	local includedParamNames = {}
	query.count = nil
	query.offset = nil

	-- We use table format when the include parameter is used to make sure we can
	-- differentiate between the results in case more than one item is included
	local dplStringInclude =
[=[
{{#dpl:
|noresultsheader=@@
|count=%s
|offset=%s
|%s
|table=,
|tablerow=%s
}}]=]

	-- Table format requires an include statement so we use format instead.
	-- This is also a lot faster than adding an empty include statement
	local dplStringNoInclude =
[=[
{{#dpl:
|noresultsheader=@@
|count=%s
|offset=%s
|%s
|format=,¦-¦[[%%PAGE%%¦]],,
}}]=]

	-- Auto generate more than one dpl if count > 500
	-- The results of these are later combined
	for i = 1, math.ceil( count / 500 ) do
		local params = {}

		for k, v in pairs( query ) do
			if k == 'include' then
				v, includedParamNames = formatInclude( v )
				usesInclude =  true
			end

			if type( v ) == 'table' then
				for _, x in ipairs( v ) do
					table.insert( params, k .. '=' .. tostring( x ):gsub( '|', '¦' ) )
				end
			else
				table.insert( params, k .. '=' .. tostring( v ):gsub( '|', '¦' ) )
			end
		end

		if usesInclude then
			table.insert( queries, string.format(
				dplStringInclude,
				count > 500 and 500 or count,
				offset,
				table.concat( params, '\n|' ),
				string.rep( dataContentMarker..'%%'..dataContentMarker..',', #includedParamNames )
			) )
		else
			table.insert( queries, string.format(
				dplStringNoInclude,
				count > 500 and 500 or count,
				offset,
				table.concat( params, '\n|' )
			) )
		end

		count = count - 500
		offset = offset + 500
	end

	table.insert( allIncludedParamNames, includedParamNames )

	return table.concat( queries )
end

local function toTable( query )
	local includedParamNames = table.remove( allIncludedParamNames, 1 )
	local usesInclude = #includedParamNames > 0
	local res = {}

	query = query:gsub( '<p>Extension:DynamicPageList .-</p>', function(item) res.error = item; return '' end )

	if query:find( '^@@' ) then -- @@ is used when no result is found
		return res
	end

	if usesInclude then
		query = query:gsub( '\127\'"`UNIQ%-%-nowiki%-%x+%-QINU`"\'\127', function(item) return '<nowiki>' .. item .. '</nowiki>' end ) -- Unstrip nowiki so we can clean their content
		query = mw.text.unstripNoWiki( query )
		query = query:gsub( dataContentMarker..'(.-)'..dataContentMarker, escape )
		query = query:gsub( '{|.-|%-', '' ) -- Remove the header of the table
		-- Replace the footer of the table width a row indicator. This effectively
		-- combines the output of multiple dpl queries when count > 500
		query = query:gsub( '|}', '|-' )
	end

	query = trim( query )
	query = split( query, '|-', true ) -- Results of the returned pages are separated by |-

	for _, v in ipairs( query ) do
		if v:find( '%S' ) and not v:find( '^@@' ) then
			v = trim( v )
			local title = v:match( '^|%[%[(.-)|' )
			local rawDataList = v:match( '^|.-|.-|(.*)' ) -- This is everything after the title

			if not usesInclude then
				if title and title ~= '' then
					table.insert( res, title )
				end
			else
				-- When multiple includes are used (e.g. include={Template1},{Template2} or include={Template}:1:2) its results are separated by a pipe
				rawDataList = split( rawDataList, '|', true )
				local cleanedDataList = {}

				for incIndex, dataItem in ipairs( rawDataList ) do
					dataItem = unEscape( dataItem )
					-- When we include an entire template we use the %ARGS% parameter supplied by dpl.
					-- However all | characters are repaced with §, e.g.:
					-- §nameLessParam
					-- §param = text [[wowee§link text]]
					-- §param2 = text {{something§something else}}
					dataItem = dataItem:gsub( '%b{}', function(x) return x:gsub( '§', '|' ) end ) -- Restore pipe characters inside links and templates
					dataItem = dataItem:gsub( '%b[]', function(x) return x:gsub( '§', '|' ) end )
					dataItem = dataItem:gsub( '<nowiki>(.-)</nowiki>', function(x) return mw.getCurrentFrame():extensionTag( 'nowiki', x ) end ) -- Restrip nowiki
					dataItem = trim( dataItem )

					if includedParamNames[ incIndex ].isTemplate and includedParamNames[ incIndex ].includeAll then -- Check if we included a full template
						local _dataItem = {}

						if dataItem ~= '' then
							dataItem = split( dataItem:sub( 3 ), '§' ) -- The sub(3) removes the first § at the start. § is 2 bytes wide so start at index 3

							for i, item in ipairs( dataItem ) do
								if item:find( '=' ) then -- Check if the parameter is named or unnamed
									local param, value = item:match( '^%s*(.-)%s*=%s*(.-)%s*$' )
									_dataItem[ param ] = value
								else
									table.insert( _dataItem, trim( item ) )
								end
							end
						end

						dataItem = _dataItem
					end

					if includedParamNames[ incIndex ].isTemplate and not includedParamNames[ incIndex ].includeAll then -- This means there was an include in the form 'include = {template}:param'
						local templateName = includedParamNames[ incIndex ].name
						local paramName = includedParamNames[ incIndex ].param
						paramName = tonumber( paramName ) or paramName -- Keep as string if tonumber fails
						cleanedDataList[ templateName ] = cleanedDataList[ templateName ] or {}
						cleanedDataList[ templateName ][ paramName ] = dataItem
					else
						cleanedDataList[ includedParamNames[ incIndex ].name ] = dataItem
					end
				end

				if title and title ~= '' then
					table.insert( res, { title=title, include=cleanedDataList } )
				end
			end
		end
	end

	return res
end

-- Accepts a series of tables each containig the settings for a dpl query.
-- Combinig multiple dpl queries yields better performance than doing them sequentially
function dpl.ask( ... )
	local queries = { ... }

	for i = 1, #queries do
		checkType( 'Module:DPLlua.ask', i, queries[i], 'table' )
		removeFormattingSettings( queries[i] )
		queries[i] = formatDpl( queries[i] )
	end

	queries = table.concat( queries, '$@µ@$' )
	local time = os.clock()
	queries = mw.getCurrentFrame():preprocess( queries )
	time = os.clock() - time
	queries = split( queries, '$@µ@$', true )

	for i = 1, #queries do
		queries[i] = toTable( queries[i] )
		queries[i].time = time
	end

	return unpack( queries )
end

return dpl
-- </nowiki>