Модуль:CenturyMetaCat (Bk;rl,&CenturyMetaCat)

Перейти к навигации Перейти к поиску
Документация

Модуль используется для полосы навигации и автокатегоризации категорий по векам (для категорий с заголовком, включающим «<римские цифры> век/века/веке»).

Возможности

[править код]
  • Определяет век, тысячелетие и эру (до н. э. / н. э.).
  • Обработка стран:
    • Определяет страну из заголовка в любом падеже.
    • Позволяет изменить падеж страны для категорий.
    • Определяет, в каких частях света расположена страна и публикует их в выбранном падеже.
    • Определяет, в какое государство входит (входила) страна, исходя из текущего века, позволяя опубликовать категории для любых государств в одном формате, либо указывать отдельные категории для выбранных государств.
  • Позволяет проверить существование категории и опубликовать одну или несколько замен для неё.
  • Добавляет {{автоиндекс}} (появляется от 200 статей, расширенный индекс от 1200 статей).
  • Создаёт навигационную линейку по векам, с возможностями:
    • Задавать min/max век в линейке.
    • Автоматически отслеживает min/max век существования отдельных стран и выдаёт ошибку при выходе за предел.
    • Позволяет указывать количество элементов в линейке.
  • Добавляет категории.

Используемые списки данных для стран:

Использование

[править код]
{{#invoke:CenturyMetaCat|main
|Мир в <век> веке по странам
|Мир <тысячелетие>-го тысячелетия <страны>!<ключ>
|Мир по векам <в части света>!<ключ>
}}

Категория состоит из 4-х частей, разделенных ! (восклицательным знаком). Первая часть — название категории, вторая часть — ключ сортировки . Третья и четвёртая части — с какого века публиковать категорию и на каком остановиться. Части 2—4 необязательны.

Примеры:

  • |Мир по векам! <ключ> — добавлять категорию «Мир по векам» с ключом сортировки <пробел><ключ>.
  • |Графы Средних веков!<ключ>!5!15 — добавлять категорию «Графы Средних веков» в категории с V по XV века.
  • |Книги в общественном достоянии!<ключ>!!19 — добавлять категорию «Книги в общественном достоянии» во все категории до XIX века включительно.

Переменные

[править код]
  • <век> — век римскими цифрами без слова «век»
  • <тысячелетие> — тысячелетие числом (без окончания -е/-м/-го)
  • <ключ> — ключ сортировки, н. э. — номер века числом, до н. э. — отрицательное число начиная с -99 (-99 == I век до н. э. -98 == II век до н. э. и т. д.); для корректной сортировки у годов до н. э. перед минусом добавляется 0
  • <страна><страны>, <в стране> — страна в необходимом падеже
  • <часть света><части света>, <в части света> — часть света в необходимом падеже
  • <государство><государства>, <в государстве> — государство в необходимом падеже
    • <государство:Название><государства:Название>, <в государстве:Название> — дополнительная проверка, позволяющая публиковать категорию только для стран, входящих в конкретное государство. Использование символа ^ перед названием государства, наоборот, исключает его из публикации среди всех остальных государств. Можно исключать сразу несколько государств, отделяя каждое из них символом ^. Для установки сложных условий отображения см. Модуль:CountryMetaCat/State.

Именительный, родительный и предложный падежи для стран, частей света и государств подставляются автоматически, соответственно указанным переменным. Вариант предложного падежа у стран и государств автоматически выводится с нужным предлогом «в/во/на». Для частей света в предложном падеже автоматически ставится предлог «в».

Следующие символы, указанные перед названием категории, осуществляют механизм проверки на существование категорий:

  • ? — категория публикуется только если она существует.
  • ~ — является заменой для несуществующей категории ?. Обязательно должна следовать сразу за ней на следующей строке, иначе игнорируется. Замены публикуются без проверок на существование. Для одной проверяемой категории может указываться несколько замен подряд.

Для отдельных стран, расположенных на двух частях света или входящих в два государства, выполняется механизм раздваивания категорий с соответствующими переменными. Если переменная указана лишь в качестве ключа сортировки, то категория публикуется только один раз. Проверка на существование категорий осуществляется для каждой из частей света или государства в названии. Если одна из категорий не существует, то будет опубликована замена для соответствующей переменной.

Полная версия

[править код]
{{#invoke:CenturyMetaCat|main
|Категория 1![ключ сортировки]![диапазон веков от]![диапазон веков до]
|?Категория 2![ключ сортировки]![диапазон веков от]![диапазон веков до]
|~Категория 3![ключ сортировки]![диапазон веков от]![диапазон веков до]
...
|Категория N[...]
|min = до какого века рисовать линейку слева, по умолчанию -39 (0 — рисовать только века нашей эры)
|max = до какого века рисовать линейку справа, по умолчанию 21
|range = сколько веков в линейке слева и справа, по умолчанию 5
}}

Дополнительные параметры:

|title = заголовок страницы, используемый вместо текущего
|noindex = 1 (указывается, если необходимо отключить добавления шаблона индекса)
|nonav = 1 (указывается, если необходимо отключить добавления навигационной линейки)

Дополнительные функции

[править код]

expand

  • заменяет <век> на текущий, по необходимости добавив «до н. э.»
  • заменяет <тысячелетие> на текущее, по необходимости добавив «до н. э.»
  • заменяет <ключ> на ключ сортировки

Например, {{#invoke:CenturyMetaCat|expand|Мир в <век> веке}} на странице Категория:Земля I века до н. э. вернёт Мир в I веке до н. э..

Страны и части света функция не обрабатывает.

century_from_title

Возвращает век из заголовка числом, для веков до н. э. с минусом.

Категории отслеживания

[править код]

См. также

[править код]
---*- mode: lua; coding: utf-8; -*-
local p = {}

-- Константы для римских цифр
local ROMAN = {
	I = 1,
	IV = 4,
	V = 5,
	IX = 9,
	X = 10
}

-- Переменные
local cent -- век, положительное число
local roman_cent -- век римскими цифрами
local BC -- 0 == н.э., 1 == до н.э.
local templ -- строка-шаблон вида 'Мир в %s веке%s'
local title = mw.title.getCurrentTitle().text

-- Опции
local cent_min = -39 -- минимальный век
local cent_max = 21 -- максимальный век (XXI век)
local range = 5

-- Импортируемые функции
local getArgs = require('Module:Arguments').getArgs
local sparseIpairs = require('Module:TableTools').sparseIpairs
local toroman = require('Module:Roman').convert
local getStyles = require('Модуль:Индекс категории').getStyles
local gsub = mw.ustring.gsub
local findCountry = require('Модуль:Find country')
local countryModule = require('Модуль:CountryMetaCat')

-- Инициализация трекера для ошибок
local error_list = {}
local century_range_error = nil
local country_error_flag = false
local unique_errors = {}

------------------ Ошибки ------------------
-- Сбор и обработка ошибок
local function add_error(error_code, additional_info)
	local error_specific = {
		[1] = 'Ошибка: век не найден.',
		[2] = 'Минимальный век, ограниченный шаблоном: ' .. (additional_info or "") .. '.',
		[3] = 'Максимальный век, ограниченный шаблоном: ' .. (additional_info or "") .. '.',
		[4] = 'Минимальный век для ' .. (additional_info or "") .. '.',
		[5] = 'Максимальный век для ' .. (additional_info or "") .. '.',
		[6] = 'Ошибка: страна не найдена.',
		[7] = 'Ошибка: часть света для страны не найдена.',
		[8] = 'Ошибка: обнаружено два века.'
	}
	if error_code >= 2 and error_code <= 5 then
		if not century_range_error then
			century_range_error = {message = 'Ошибка: век не попадает в заданный диапазон.', details = {}}
			table.insert(error_list, century_range_error)
		end
		table.insert(century_range_error.details, error_specific[error_code])
	else
		-- Для остальных ошибок проверяем уникальность
		local error_message = '<span class="error">' .. error_specific[error_code] .. '</span>'
		if not unique_errors[error_message] then
			unique_errors[error_message] = true
			table.insert(error_list, {message = error_message})
		end
	end
end

-- Публикация всех ошибок в едином блоке
local function publish_errors()
	local error_category = '[[Категория:Википедия:Страницы с некорректным использованием модуля CenturyMetaCat]]'
	if #error_list == 0 then
		return ''
	end
	local result = '<div class="error-list">'
	for _, err in ipairs(error_list) do
		if err.details then
			result = result .. '<span class="error">' .. err.message
			for _, detail in ipairs(err.details) do
				result = result .. ' ' .. detail
			end
			result = result .. '</span>'
		else
			result = result .. err.message
		end
	end
	result = result .. '</div>'
	result = result .. error_category
	return result
end

------------------ Считывание и обработка веков ------------------
local function roman_to_int(s)
	local i = 1
	local num = 0
	while i <= s:len() do
		local c
		if i < s:len() then
			c = ROMAN[s:sub(i, i+1)]
		end
		if c then
			num = num + c
			i = i + 2
		else
			num = num + ROMAN[s:sub(i, i)]
			i = i + 1
		end
	end
	return num
end
-- Считывание века из строки
local function get_cent(t)
	local centuries = {}
	for rc in mw.ustring.gmatch(t, '([IVX]+) век') do
		table.insert(centuries, rc)
	end

	if #centuries == 0 then
		roman_cent = nil
		add_error(1) -- Ошибка "не найден"
		return nil
	elseif #centuries > 1 then
		add_error(8) -- Ошибка "обнаружено два"
		return nil
	end
	roman_cent = centuries[1]
	cent = roman_to_int(roman_cent)
	return cent
end

-- Замена плейсхолдеров (век, тысячелетие, ключ) на реальные значения
local function do_expand(s)
	-- <век> - век римскими цифрами без слова "век" (XVII)
	-- <тысячелетие> - тысячелетие числом (без добавления -е/-м/-го)
	-- <ключ> - ключ сортировки, н.э. - номер века числом,
	-- до н.э. - отрицательное число начиная с -99 (-99 == I век до н.э. -98 == II век до н.э. и т.д.)
	local mil = math.floor((cent - 1) / 10) + 1

	-- Корректировка тысячелетия для второго тысячелетия
	if mil == 2 then
		s = gsub(s, ' в <тысячелетие>', ' во <тысячелетие>')
	end
	-- Логика замены для веков
	if cent == 2 then
		s = gsub(s, 'в <век> веке', 'во <век> веке')
	end
	if cent == 11 then
		s = gsub(s, 'о <век> веке', 'об <век> веке')
	end
	-- Замены для до нашей эры
	if BC == 1 then
		s = gsub(s, '<век> (век[еа]?)', roman_cent .. ' %1 до н. э.')
		s = gsub(s, '<тысячелетие>(-[емг][о]? тысячелети[еия])', mil..'%1 до н. э.')  -- 2-е/2-м/2-го
		s = gsub(s, '<ключ>', '0' .. (cent - 100))  -- ключ для веков до н.э.
	else
		-- Замены для нашей эры
		s = gsub(s, '<век>', roman_cent)
		s = gsub(s, '<тысячелетие>', mil)
		s = gsub(s, '<ключ>', cent)  -- ключ для веков нашей эры
	end
	return s
end

------------------ Обработка min/max ------------------
-- Поиск данных о стране в JSON-файле по названию или алиасу
local function find_country_in_json(country_name)
	local country_data = mw.loadJsonData('Модуль:YearMetaCat2/country-years.json')
	for _, country in ipairs(country_data.countries) do
		if country.name == country_name then
			return country
		end
		if country.aliases then
			for _, alias in ipairs(country.aliases) do
				if alias == country_name then
					return country
				end
			end
		end
	end
	return nil
end

-- Проверка, попадает ли век в диапазон страны или вручную заданные значения
local function check_century_in_bounds(args)
	args = args or {}
	local country_name = findCountry.findcountryinstring(title)
	local country_data = find_country_in_json(country_name)
	-- Корректировка для до н.э.
	local cent_adjusted = BC == 1 and -cent or cent
	-- Ручные ограничения min и max
	local manual_min = tonumber(args['min'])
	local manual_max = tonumber(args['max'])
	-- Преобразуем годы из JSON в десятилетия, если данные найдены
	local country_min = country_data and country_data.min and math.floor((country_data.min - 1) / 100) + 1 or nil
	local country_max = country_data and country_data.max and math.floor((country_data.max - 1) / 100) + 1 or nil
	-- Определение активных границ
	local effective_min = manual_min or country_min
	local effective_max = manual_max or country_max
	-- Проверка минимального значения
	if effective_min and cent_adjusted < effective_min then
		if manual_min then
			-- Если задано вручную
			add_error(2, tostring(effective_min))
		elseif country_data then
			-- Если данные из страны
			add_error(4, string.format('%s: %d (минимальный год: %d)', country_name, effective_min, country_data.min))
		end
	end
	-- Проверка максимального значения
	if effective_max and cent_adjusted > effective_max then
		if manual_max then
			-- Если задано вручную
			add_error(3, tostring(effective_max))
		elseif country_data then
			-- Если данные из страны
			add_error(5, string.format('%s: %d (максимальный год: %d)', country_name, effective_max, country_data.max))
		end
	end
end

--------------- Считывание и обработка стран ---------------
-- Проверка на наличие плейсхолдеров, связанных со странами
local function has_country_placeholders(s)
	local placeholders = {
		'<страна>', '<страны>', '<в стране>',
		'<часть света>', '<части света>', '<в части света>',
		'<государство>', '<государства>', '<в государстве>'
	}
	-- Проверка на стандартные плейсхолдеры
	for _, placeholder in ipairs(placeholders) do
		if s:find(placeholder, 1, true) then
			return true
		end
	end
	-- Проверка на плейсхолдеры с указанием названия страны (например, <государство:Название страны>)
	local complex_placeholders = {
		'<государство:[^>]+>',
		'<государства:[^>]+>',
		'<в государстве:[^>]+>'
	}
	for _, pattern in ipairs(complex_placeholders) do
		if s:find(pattern) then
			return true
		end
	end
	return false
end

-- Обработка стран, частей света стран или государств
local function process_country_placeholders(s, title, current_cent)
	if type(s) ~= 'string' then return {}, nil end
	local result_lines = {}
	local added_categories = {}
	-- Вызываем resolve_country с нужными параметрами
	local country_result = countryModule.resolve_country({
		[1] = s,
		title = title,
		type = "century",
		time = current_cent
	})
	if country_result then
		-- Обработка основного результата
		if country_result.result and country_result.result ~= "" then
			table.insert(result_lines, {text = country_result.result, type = "main"})
		end
		-- Обработка дополнительного результата
		if country_result.extra_result and country_result.extra_result ~= "" then
			table.insert(result_lines, {text = country_result.extra_result, type = "extra"})
		end
		-- Обработка ошибок от country_module
		if country_result.error and country_result.error > 0 then
			add_error(country_result.error == 1 and 6 or 7)
			country_error_flag = true
		end
	end
	return result_lines
end

------------------ Форматирование строк ------------------
-- Формирование шаблона строки для отображения года с учётом до н. э.
local function get_templ(s)
	-- Формируем строку-шаблон вида: 'Мир в XI веке до н. э.' -> 'Мир в %s веке%s'
	local t
	t, BC = gsub(s, '[IVX]+ (век[еа]?) до н%. э%.', '%%s %1%%s')
	local n = BC
	if BC ~= 1 then
		t, n = gsub(s, '[IVX]+ (век[еа]?)', '%%s %1%%s')
	end
	if n ~= 1 then  -- Ошибка, если совпадений нет или их больше одного
		add_error(1)
	end
	-- в/во, о/об
	t = gsub(t, 'во %%s веке', 'в %%s веке')
	templ = gsub(t, 'об %%s веке', 'о %%s веке')
	return nil
end

-- Форматирование года с учётом до н. э.
local function format(c, wiki)
	local bcs, t
	if c < 1 then
		c = 1 - c
		bcs = ' до н. э.'
		t = '−'..toroman(c)
	else
		bcs = ''
		t = toroman(c)
	end
	local s
	if wiki then
		-- в/во, о/об
		local tt = templ
		if c == 2 then
			tt = gsub(tt, 'в %%s веке', 'во %%s веке')
		end
		if c == 11 then
			tt = gsub(tt, 'о %%s веке', 'об %%s веке')
		end
		s = string.format(tt, toroman(c), bcs)
		s = string.format('[[:К:%s|%s]]', s, t)
	else
		s = t
	end
	return s
end

------------------ Список категорий ------------------
-- Проверка на существование категории
local function category_exists(category_name)
	if not category_name or category_name == '' then return false end
	-- Удаление символов ? ~ вначале или ! с текстом вконце
	category_name = mw.ustring.match(category_name, "^[%?~]*(.-)!") or category_name
	local title = mw.title.new('Категория:' .. category_name)
	return title and title.exists
end

-- Основная обработка категорий
local function cats(args)
	local ret = ''
	local added_categories = {}
	local lines = {}

	-- Вспомогательная функция для добавления категории
	local function add_category(text)
		local processed = do_expand(text:gsub("!", "|"))
		local categories = mw.text.split(processed, "|")
		local cat_name = categories[1]
		local sort_key = categories[2] or ""
		local cmin = tonumber(categories[3])
		local cmax = tonumber(categories[4])
		
		-- Логика проверки диапазона веков, встроенная из process_category_range
		local cc = cent
		if BC == 1 then cc = -cent end
		
		if (not cmin or cc >= cmin) and (not cmax or cc <= cmax) then
			if not added_categories[cat_name] then
				ret = ret .. string.format('[[Категория:%s%s]]',
					cat_name,
					sort_key ~= "" and ('|' .. sort_key) or ''
				)
				added_categories[cat_name] = true
				return true
			end
		end
		return false
	end

	-- Обработка входных аргументов и заполнение lines
	for i, arg in sparseIpairs(args) do
		if type(arg) == "string" and arg ~= "" then
			if has_country_placeholders(arg) then
				local result = process_country_placeholders(arg, title, cent)
				lines[i] = {
					original = arg,
					results = result or {},
					is_placeholder = true
				}
			else
				local text = do_expand(arg)
				lines[i] = {
					original = arg,
					results = {
						{text = text, type = "main"},
						{text = text, type = "extra"}
					},
					is_placeholder = false
				}
			end
		end
	end

	local i = 1
	while i <= #lines do
		local line = lines[i]
		if line then
			local first_char = mw.ustring.sub(line.original, 1, 1)
			if first_char == '?' then
				local exists = {main = false, extra = false}
				local questions = {main = {}, extra = {}}

				-- Проверяем существование категорий
				for _, result in ipairs(line.results) do
					local text = result.text:sub(2):gsub("^%s+", "")
					local cat_name = do_expand(text:match("^(.-)!") or text)
					questions[result.type][cat_name] = text
					if category_exists(cat_name) then
						exists[result.type] = true
						add_category(result.text:sub(2))
					end
				end

				local j = i + 1
				while j <= #lines and mw.ustring.sub(lines[j].original, 1, 1) == '~' do
					for _, result in ipairs(lines[j].results) do
						if not exists[result.type] and next(questions[result.type]) then
							add_category(result.text:sub(2))
						end
					end
					j = j + 1
				end
				i = j - 1

			elseif first_char ~= '~' then
				for _, result in ipairs(line.results) do
					if result.text and result.text ~= "" then
						add_category(result.text)
					end
				end
			end
		end
		i = i + 1
	end

	return ret
end

------------------ Навигационный блок ------------------
local function navbox()
	local c = cent
	-- Корректировка для до н. э.
	local c = (BC == 1) and 1 - cent or cent
	-- Создаем HTML-элемент для навигационной панели
	local wt = mw.html.create('div'):addClass('ts-module-Индекс_категории hlist')
	local row = wt:tag('ul')
	-- Корректировка min и max для до н. э.
	local adjusted_min = cent_min <= 0 and cent_min + 1 or cent_min
	local adjusted_max = cent_max <= 0 and cent_max + 1 or cent_max
	-- Поиск данных о стране
	local country_data = find_country_in_json(findCountry.findcountryinstring(title))
	-- Проверка, что данные о стране существуют и у них есть min и max
	local country_min_century = math.max(adjusted_min, country_data and country_data.min and math.floor((country_data.min - 1) / 100) + 1 or adjusted_min)
	local country_max_century = math.min(adjusted_max, country_data and country_data.max and math.floor((country_data.max - 1) / 100) + 1 or adjusted_max)
	-- Устанавливаем начальный и конечный диапазоны для линейки с учётом range
	local cstart = math.max(country_min_century, c - range)
	local cend = math.min(country_max_century, c + range)
	-- Если диапазон некорректный, возвращаем пустую строку
	if cend < cstart then return "" end
	-- Добавляем элементы в навигационную полоску
	for i = cstart, cend do
		row:tag('li'):wikitext(format(i, true))
	end
	return getStyles() .. tostring(wt)
end

------------------ Вывод ------------------
function p.main(frame)
	local args = getArgs(frame)
	title = args['title'] or title
	range = tonumber(args['range'] or range)

	if mw.title.getCurrentTitle().namespace == 10 then  -- проверка пространства шаблонов
		return	"[[Категория:Шаблоны, использующие модуль CenturyMetaCat]]" ..
				"[[Категория:Шаблоны, использующие индекс категории (автоматический)]]"
	end

	-- Обработка вручную заданных min и max
	cent_min = tonumber(args['min'] or cent_min)
	cent_max = tonumber(args['max'] or cent_max)

	-- Нахождение века по заголовку страницы
	cent = get_cent(title)
	if not cent then
		return publish_errors()  -- Возврат ошибок и прекращаем выполнение, если год не найден
	end

	-- Создание шаблона-строки
	get_templ(title)

	-- Стандартная категоризация
	local categories = cats(args)

	-- Проверка, попадает ли век в допустимые границы
	check_century_in_bounds(args)
	local output = ""

	-- Навигационная полоска с отключением
	if args['nonav'] ~= "1" then
		output = output .. navbox()
	end

	-- Автоиндекс с отключением
	if args['noindex'] ~= "1" then
		output = output .. mw.getCurrentFrame():preprocess('{{индекс категории (автоматический)}}\n')
	end

	-- Преобразование таблицы категорий в строку, если это таблица
	if type(categories) == "table" then
		local flat_categories = {}
		for _, value in ipairs(categories) do
			table.insert(flat_categories, value.text)
		end
		categories = table.concat(flat_categories, '')
	end

	output = output .. publish_errors()
	return output .. (categories or "")
end

-- Вспомогательная функция для развёртывания
function p.expand(frame)
	local args = getArgs(frame)
	title = args['title'] or title
	cent = get_cent(title)
	if not cent then
		return publish_errors()
	end
	BC = mw.ustring.find(title, '[IVX]+ век[еа]? до н%. э%.')
	if BC then
		BC = 1
	else
		BC = 0
	end
	return do_expand(args[1])
end

function p.century_from_title(frame)
	local args = getArgs(frame)
	title = args['title'] or title
	BC = mw.ustring.find(title, '[IVX]+ век[еа]? до н%. э%.')
	get_cent(title)
	if BC then
		return -cent
	end
	return cent
end
return p