Modulo:ChronoSpine/manual

Da Wikitrek.
Vai alla navigazione Vai alla ricerca

La documentazione per questo modulo può essere creata in Modulo:ChronoSpine/manual/man

--[[
Module:ChronoSpine/manual
============================

Purpose
-------
The "manual" data-source adapter: turns numbered parameter sets typed
directly into a template call (`data1=`, `voce1=`, `stellare1=`, ...)
into normalised entries (Module:ChronoSpine/entry shape), ready for
the sort/group/render stages of the pipeline (Steps 3-4).

This module is `tempo`-independent: nothing here reads or branches on
`tempo` (reale/narrativo). A bare year is a valid date either way, and
the differences between the two modes (which date SMW property to
prefer, the "DS" stardate display prefix, year auto-links, ...) only
matter once entries reach grouping/rendering, not while parsing a
single manual entry. If a `tempo`-specific validation rule ever
surfaces, it belongs here as an explicit exception -- flagged in the
design discussion rather than assumed away.

Discovery of numbered sets
---------------------------
Unlike Wikipedia's `Module:Timeline of release years` (which stops at
the first gap in a contiguous 1997, 1998, 1999, ... sequence), this
module scans the ENTIRE set of frame arguments for every field's
aliases (not just `date`), and processes every distinct index for
which at least one field has a non-blank value -- gaps are fine, and
an index where every field is blank or absent is a leftover unused
template slot, not a mistake (no problem is logged for it at all).

Problem severity
------------------
Each numbered set can go wrong in two different ways:

  * FATAL (the whole entry is left out of `entries`, and only appears
    in `problems`): the `date` field is missing entirely, or present
    but fails to parse; or the `label` field is missing. There is no
    sensible position (no date) or no name (no label) to put on the
    timeline.
  * NON-FATAL (the entry still goes into `entries`; the one bad field
    is simply left unset, and a problem is still logged so the editor
    can fix it): the `stardate` field is present but fails to parse.
    A stardate is only ever an in-year tie-breaker, never a
    substitute for the date, so losing it does not make the entry
    unplaceable.

`?` typed for `date` is NOT a problem of either kind -- it is the
normal "uncertain date" path already handled by
Module:ChronoSpine/date (`ok = true, incerta = true`), so those
entries go straight into `entries`.

What this module does NOT do
------------------------------
It never turns a problem `code` into displayed text (that is
Module:ChronoSpine/i18n's M.message), and it never decides how
problems reach the reader -- a note box, a tracking category, or
anything else (that is the renderer, Step 4, using
Module:ChronoSpine/config for the category name). `/manual` only
produces the structured data.
--]]

local M = {}

local i18n = require("Module:ChronoSpine/i18n")
local entry = require("Module:ChronoSpine/entry")
local date_parser = require("Module:ChronoSpine/date")
local stardate_parser = require("Module:ChronoSpine/stardate")

--[[
discover_indices(args)
------------------------
Private helper. Finds every numbered-set index that has at least one
non-blank field, across every field's aliases (see M.field_aliases in
Module:ChronoSpine/i18n) -- not just the `date` field -- so that, for
example, a set with `voce7` filled in but `data7` forgotten is still
discovered (and therefore still gets a "missing-date" problem logged
for it), rather than being silently invisible to this module simply
because nothing ever looked for a `data7`/`date7` key.

Parameters:
  args (table) -- the raw frame arguments: a plain table of parameter
                  name -> string value (frame:getParent().args or
                  equivalent).

Returns:
  (table) -- a list of indices (numbers), sorted ascending.
--]]
local function discover_indices(args)
	local found = {} -- set: index (number) -> true

	for _, aliases in pairs(i18n.field_aliases) do
		for _, alias in ipairs(aliases) do
			-- `alias` is always one of our own hardcoded strings from
			-- Module:ChronoSpine/i18n, never derived from wiki
			-- content, so it is safe to splice directly into a Lua
			-- pattern without needing to escape it.
			local pattern = "^" .. alias .. "(%d+)$"

			for key, value in pairs(args) do
				if type(key) == "string" then
					local index_text = key:match(pattern)
					if index_text then
						local value_text = (type(value) == "string") and value or tostring(value)
						local trimmed = value_text:match("^%s*(.-)%s*$")
						if trimmed ~= "" then
							found[tonumber(index_text)] = true
						end
					end
				end
			end
		end
	end

	local indices = {}
	for index in pairs(found) do
		table.insert(indices, index)
	end
	table.sort(indices)
	return indices
end

--[[
build_one(args, index)
------------------------
Private helper. Resolves and validates a single numbered set (e.g.
everything with suffix `3`: `data3`, `voce3`, `stellare3`, ...).

Parameters:
  args  (table)  -- the raw frame arguments, as in discover_indices().
  index (number) -- the numbered set's index.

Returns:
  ok_entry (table|nil) -- a normalised entry (see
                          Module:ChronoSpine/entry), or nil if `date`
                          or `label` was fatally missing/invalid.
  problems (table)     -- a list (possibly empty) of problem tables,
                          each { ordine, field, code, raw }. Populated
                          regardless of whether `ok_entry` is nil --
                          a non-fatal problem can exist alongside a
                          perfectly good entry.
--]]
local function build_one(args, index)
	local problems = {}
	local fatal = false

	-- `date`: fatal if missing or unparseable.
	local date_raw = i18n.resolve(args, "date", index)
	local date_result
	if date_raw == nil then
		fatal = true
		table.insert(problems, { ordine = index, field = "date", code = "missing-date", raw = nil })
	else
		date_result = date_parser.parse(date_raw)
		if not date_result.ok then
			fatal = true
			table.insert(problems, {
				ordine = index,
				field = "date",
				code = "date:" .. date_result.code,
				raw = date_raw,
			})
		end
	end

	-- `label`: fatal if missing. (No parser to run -- any non-blank
	-- text is a valid label, wikitext included.)
	local label_raw = i18n.resolve(args, "label", index)
	if label_raw == nil then
		fatal = true
		table.insert(problems, { ordine = index, field = "label", code = "missing-label", raw = nil })
	end

	if fatal then
		-- No point even attempting the non-fatal fields below: there
		-- is nothing valid to attach them to.
		return nil, problems
	end

	-- `stardate`: NON-fatal. A bad value just means the entry has no
	-- tie-breaker; it still gets placed on the spine by its date.
	local stellare_value
	local stardate_raw = i18n.resolve(args, "stardate", index)
	if stardate_raw ~= nil then
		local stardate_result = stardate_parser.parse(stardate_raw)
		if stardate_result.ok then
			stellare_value = stardate_result.value
		else
			table.insert(problems, {
				ordine = index,
				field = "stardate",
				code = "stardate:" .. stardate_result.code,
				raw = stardate_raw,
			})
		end
	end

	local new_entry = entry.new({
		anno      = date_result.anno,
		mese      = date_result.mese,
		giorno    = date_result.giorno,
		circa     = date_result.circa,
		incerta   = date_result.incerta,
		fine      = date_result.fine,
		stellare  = stellare_value,
		voce      = label_raw,
		fonte     = i18n.resolve(args, "source", index),
		nota      = i18n.resolve(args, "note", index),
		gruppo    = i18n.resolve(args, "group", index),
		categoria = i18n.resolve(args, "category", index),
		stato     = i18n.resolve(args, "status", index),
		ordine    = index,
	})

	return new_entry, problems
end

--[[
M.build(args)
--------------
The adapter's single public entry point. Discovers every numbered set
present in `args`, validates and normalises each one, and returns
everything found.

Parameters:
  args (table) -- the raw frame arguments (frame:getParent().args or
                  equivalent).

Returns:
  (table) {
    entries  = { <normalised entry>, ... },  -- see Module:ChronoSpine/entry
    problems = { { ordine, field, code, raw }, ... },
  }

  Both lists may be empty. `entries` is NOT sorted (that is Step 3's
  job); it reflects discovery order, which is already ascending by
  index since discover_indices() sorts it.
--]]
function M.build(args)
	local indices = discover_indices(args)

	local entries = {}
	local problems = {}

	for _, index in ipairs(indices) do
		local ok_entry, entry_problems = build_one(args, index)
		if ok_entry then
			table.insert(entries, ok_entry)
		end
		for _, problem in ipairs(entry_problems) do
			table.insert(problems, problem)
		end
	end

	return { entries = entries, problems = problems }
end

return M