Module:SOM.meta.Page

From SunshinePPS Wiki

Documentation for this module may be created at Module:SOM.meta.Page/doc

require("Module:No globals")

local References = require("Module:SOM.util.References")
local StringBuffer = require("Module:SOM.util.StringBuffer")
local util = require("Module:SOM.util")


local function compute_renderer(specifier, class)
	if type(specifier) == "function" then
		return specifier
	elseif class and class[specifier] then
		return class[specifier]
	elseif class and class.render then
		return class.render
	else
		return mw.dumpObject
	end
end



local p = {}

function p.generate_page(args)
	local page_class = args.class_name
	local parent_categories = args.parent_categories
	local category_description = args.category_description
	local attributes = args.attributes
	local form_layout = args.form_layout
	local page_layout = args.page_layout
	local class_prefix = args.class_prefix or "SOM.Page."
	local naming_advice = args.naming_advice

	local page_form = page_class
	local page_category = page_class
	local page_template = class_prefix .. page_class
	local page_module = class_prefix .. page_class

	local describe_page_form
	local produce_page_form
	local describe_page_template
	local expand_page_template


	local page = {}
	page.docs = {}

--	page.module_name = page_module
	
	function page.docs.docs()
		return {
			module_name = page_module,
			docstring = mw.ustring.format([==[
This module defines the class <code>%s</code> which defines
pages in the category [[:Category:%s]].

%s
]==], page_class, page_category, category_description),
		}
	end


	function page.page_category(frame)
		frame:callParserFunction("#default_form", {page_form})
		local buffer = StringBuffer.new()
		buffer
			:add('<div style="background-color: #EEEE88; padding: 0.5em 0.5em; margin: 1em 3em">'):nl()
			:add_uformat("To create or edit a ''%s'' page, type in a name below.  ",
				page_category)
		if naming_advice then
			buffer:add_uformat("%s  ", naming_advice)
		end
		buffer
			:add(frame:callParserFunction("#forminput", 
				{"", form = page_form, 
					["autocomplete on category"] = page_category})):nl()
			:add("</div>"):nl()
			:nl()
			:add(category_description):nl()
			:add("----"):nl()
			:add('<div style="font-size: 85%">')
			:add("Technical Details<br>"):nl()
			:add("Pages in this category are:<ul>"):nl()
			:add_uformat("<li>defined by [[Module:%s]];</li>", page_module):nl()
			:add_uformat("<li>instantiated by [[Template:%s]];</li>", page_template):nl()
			:add_uformat("<li>created and edited using [[Form:%s]].</li>", page_form):nl()
			:add("</ul>"):nl()
			:add("</div>"):nl()
			:nl()
		for i, c in ipairs(parent_categories) do
			buffer:add_uformat("[[Category:%s]]", c):nl()
		end
		return buffer:output()
	end

	function page.docs.page_category()
	  	return {
			desc = mw.ustring.format("Generates the definition for this class' category, [[:Category:%s]].  Typically, invoked by <code>SOM.util.auto_generate_page_category</code>",
										page_category),
			args = {},
			needs_frame = true,
		}
	end


	function page.page_form(frame)
		local parent = frame:getParent()
		local parent_title = frame:getParent():getTitle()
		local current_title = mw.title.getCurrentTitle().fullText
	
		parent_title = nil
	
		-- if parent_title == current_title then
		if mw.ustring.match(current_title, "^Form:") ~= nil then
			return describe_page_form(frame)
			.. "\n\n<hr>\n\n<pre>" .. frame:callParserFunction('#tag:nowiki', produce_page_form()) .. "</pre>"
		else
			return produce_page_form()
		end
	end

    function page.docs.page_form()
	  	return {
			desc = mw.ustring.format("Generates the definition for this class' form, [[Form:%s]].  Typically, invoked by <code>SOM.util.auto_generate_page_form</code>",
										page_form),
			args = {},
			needs_frame = true,
		}
    end


	function page.page_template(frame)
		local parent = frame:getParent()
		local parent_title = frame:getParent():getTitle()
		local current_title = mw.title.getCurrentTitle().fullText
		
		if parent_title == current_title then
			return describe_page_template()
		else
			return expand_page_template(parent)
		end
	end

    function page.docs.page_template()
	  	return {
			desc = mw.ustring.format("Generates the definition for the template which instantiates this class, [[Template:%s]].  Typically, invoked by <code>SOM.util.auto_generate_page_template</code>",
										page_template),
			args = {},
			needs_frame = true,
		}
	end
	if false then
    	local arg_list = {}
    	local sorted = {}
    	for k, v in pairs(attributes) do
    		table.insert(sorted, k)
    	end
    	table.sort(sorted)
    	for _, k in ipairs(sorted) do
    		local attr = attributes[k]
    		local desc
    		if attr.class.is_composite() then
    			desc = mw.ustring.format(
    				"JSON-encoded data for zero or more [[Module:%s|<code>%s</code>]]",
    				attr.class.module_name, attr.class.module_name)
    		else
    			desc = mw.ustring.format(
    				"Data for [[Module:%s|<code>%s</code>]]",
    				attr.class.module_name, attr.class.module_name)
    		end
	 		table.insert(arg_list, { attr.arg, desc })	
    	end
	  	return {
			desc = mw.ustring.format("Implements the template for this class, [[Template:%s]].  Typically, invoked by <code>SOM.util.auto_generate_page_template</code>",
										page_template),
			args = arg_list,
			needs_frame = true,
		}
    end


	function describe_page_form(frame)
		return StringBuffer.new()
			:add_uformat("This is the ''%s'' form.", page_category):nl()
			:add([==[
To create a page with this form, enter the page name below;
if a page with that name already exists, you will be sent to a form to edit that page.
]==])
			:add(frame:callParserFunction("#forminput", 
					{"", 
					form = page_form, 
					["autocomplete on category"] = page_category}))
			:output()
		end


	function produce_page_form()
		local FormTable = require("Module:SOM.util.FormTable")
		local buffer = StringBuffer.new()
		-- Start with boilerplate.
		buffer
			:add("__NOEDITSECTION__"):nl()
			:add("__NOTOC__"):nl()
			:add_uformat(
				"{{{info|create title=Create a new %s|edit title=Edit a %s}}}",
				page_category, page_category):nl()
			:add([===[
<div id="wikiPreview" style="display: none; padding-bottom: 25px; margin-bottom: 25px; border-bottom: 1px solid #AAAAAA;"></div>
]===])
	
		-- Construct the form template for the page itself.
		buffer:add_uformat("{{{for template|%s}}}", page_template):nl()
		for _, chunk in ipairs(form_layout) do
			if chunk.section2 then
				buffer:add_uformat("== %s ==", chunk.section2):nl()
			elseif chunk.section3 then
				buffer:add_uformat("=== %s ===", chunk.section3):nl()
			elseif chunk.section4 then
				buffer:add_uformat("==== %s ====", chunk.section4):nl()
			elseif chunk.section5 then
				buffer:add_uformat("===== %s =====", chunk.section5):nl()
			elseif chunk.attribute then
				local attr = attributes[chunk.attribute]
				buffer:add(attr.class.form_field{ field_name = attr.arg }):nl()
			elseif chunk.table then
				local ft = FormTable.new()
				for _, row in ipairs(chunk.table) do
					local attr = attributes[row.attribute]
					ft:row(attr.label .. ":", attr.class.form_field{ field_name = attr.arg })
				end
				buffer:add(ft):nl():nl()
			else
				util.assertf(false, "What does this chunk do?  %s", mw.dumpObject(chunk))
			end
		end
		buffer:add("{{{end template}}}"):nl()
		
		-- Add embedded form templates.
		for key, attr in pairs(attributes) do
			if attr.class.is_composite() then
				buffer:add(attr.class.form_entry{
					embed_in = mw.ustring.format("%s[%s]", page_template, attr.arg),
					hide = attr.link_to_key
				}):nl()
			end
		end
		
		return buffer:output()
	end


	function describe_page_template()
		local buffer = StringBuffer.new()
			:add("This template is used to instantiate the ")
			:add_uformat("[[Module:%s|<code>%s</code>]]", page_module, page_module)
			:add("class, creating pages in the ")
			:add_uformat("[[:Category:%s|%s]]", page_category, page_category)
			:add(" category.")
			:nl():nl()
			:add("This template has been automatically generated by the class.")
			:nl():nl()
			:add("It takes these parameters:"):nl()
-- TODO maddog get template docs from somewhere
--		for _, argdesc in ipairs(page.docs.instantiate().args) do
--			local arg, desc = unpack(argdesc)
--			buffer:add_uformat("* <code>%s</code> - %s", arg, desc):nl()
--		end
		return buffer:output()
	end


   
	function expand_page_template(frame)
		-- PageForms docs recommend calling {{#default_form|...}} in the page's
		-- defining Category... but in practice that seems to be brittle in the
		-- face of errors, probably due to caching issues.  So, we just execute
		-- it on every page, before anything else might get in the way.
		frame:callParserFunction("#default_form", {page_form})
	
		-- -------------------------------------------------
		-- Process all input data supplied by template, and capture errors
		-- -------------------------------------------------
		local i_ = {}
		local we_have_missing_data = false
		local ec = require("Module:SOM.util.ErrorCapture").new()
	
		for k, v in pairs(attributes) do
			-- (Templates/etc supplied in template parameters may not be expanded
			-- until the value is dereferenced/accessed.)
			local param = frame.args[v.arg]
			
			if v.class == nil then
				i_[k] = param
	
			elseif v.class.is_composite() then
				local override_values = {}
				if v.link_to_key then
					override_values[v.link_to_key] = mw.title.getCurrentTitle().fullText
				end
				local _, instances, missing, sub_ec = 
					ec:pcall(v.class.decode_and_attach, param or "", override_values)
				if instances then
					i_[k] = instances
					we_have_missing_data = we_have_missing_data or missing
					ec:add_ec(sub_ec)
				end
	
			else -- not composite => simple
				-- Call .attach() methods for simple values, and capture any errors.
				local _, i = ec:pcall(v.class.attach,
					{ value = param, empty_as = "missing" })
				i_[k] = i and i.value
				we_have_missing_data = we_have_missing_data or (not i)
			end
		end
	
	
		--local documents = AttachedFile.fetch_all({page_as = "page"})
	
		-- -------------------------------------------------
		-- Produce the page
		-- -------------------------------------------------
		local buffer = StringBuffer.new()
		-- Start with boilerplate.
		buffer
			:add("__NOEDITSECTION__"):nl()
			-- TODO maddog Should missing district be considered an /error/ or just
			--             missing datum?
			:add_and_nl(ec)
		-- Add content.
		for _, chunk in ipairs(page_layout) do
			if chunk.infobox then
				-- In MediaWiki:Common.css, we have disabled the "float: right"
				-- built-in to Capiunto's infobox CSS, so that we can use the
				-- responsive float utility from Bootstrap here.  This way, on
				-- narrow (mobile) screens, we don't try to float at all since
				-- there isn't enough room anyway.
				local ib = require("capiunto").create{ bodyClass = "wikitable float-md-right mx-3" }
				for _, row in ipairs(chunk.infobox) do
					if row.section then
						ib:addHeader(row.section)
					elseif row.attribute then
						local attr = attributes[row.attribute]
						local label = row.label or attr.label
						local renderer = compute_renderer(row.render, attr.class)
						ib:addRow(label, 
							renderer(i_[row.attribute], frame, row.render_args))
--						if row.render then
--							ib:addRow(attr.label, row.render(i_[row.attribute], frame))
--						elseif attr.class.render then
--							ib:addRow(attr.label, attr.class.render(i_[row.attribute]))
--						else
--							ib:addRow(attr.label, mw.dumpObject(i_[row.attribute]))
--						end
					elseif row.render_any then
						local label, value = row.render_any(i_, frame, row.render_args)
						if label ~= nil then
							ib:addRow(label, value)
						end
					else
						util.assertf(false, "Unknown infobox row %s", tostring(row and next(row)))
					end
				end
				buffer:add(ib:getHtml()):nl()
			elseif chunk.section then
				buffer:add_uformat("\n=== %s ===", chunk.section):nl()
			elseif chunk.section2 then
				buffer:add_uformat("\n== %s ==", chunk.section2):nl()
			elseif chunk.section3 then
				buffer:add_uformat("\n=== %s ===", chunk.section3):nl()
			elseif chunk.section4 then
				buffer:add_uformat("\n==== %s ====", chunk.section4):nl()
			elseif chunk.section5 then
				buffer:add_uformat("\n===== %s =====", chunk.section5):nl()
			elseif chunk.attribute then
				local attr = attributes[chunk.attribute]
				local renderer = compute_renderer(chunk.render, attr.class)
				buffer:add(renderer(i_[chunk.attribute], frame, chunk.render_args)):nl()
			elseif chunk.wikitext then
				buffer:add(chunk.wikitext):nl()
			elseif chunk.references then
				buffer:add(References.emit_references(frame)):nl()
			elseif chunk.renderer then
				chunk.renderer(i_, buffer, frame)
			elseif chunk.fact_table then
				buffer:add("FACT TABLE"):nl():nl()
			end
		end
	
		-- Add categories.
		buffer
			:add_uformat("[[Category:%s]]", page_category):nl()
			:nl()
		if we_have_missing_data then
			buffer:add(util.MISSING_DATA_CATEGORY_TAG):nl()
		end
		if not ec:is_empty() then
			buffer:add(util.BROKEN_PAGE_CATEGORY_TAG):nl()
		end
	
		return buffer:output()
	end


	return page
end


return p