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

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

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

Возможности

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

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

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

[править код]
{{#invoke:CountryMetaCat|main
|Появились <в стране>
|События <части света>!текст для сортировки
}}

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

Переменные

[править код]
  • <страна><страны>, <в стране> — страна в необходимом падеже
  • <часть света><части света>, <в части света> — часть света в необходимом падеже
  • <государство><государства>, <в государстве> — государство в необходимом падеже
    • <государство:Название><государства:Название>, <в государстве:Название> — дополнительная проверка, позволяющая публиковать категорию только для стран, входящих в конкретное государство. Использование символа ^ перед названием государства, наоборот, исключает его из публикации среди всех остальных государств. Можно исключать сразу несколько государств, отделяя каждое из них символом ^. Для установки сложных условий отображения см. Модуль:CountryMetaCat/State.

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

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

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

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

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

[править код]
{{#invoke:CountryMetaCat|main
|Категория 1![ключ сортировки]
|?Категория 2![ключ сортировки]
|~Категория 3![ключ сортировки]
...
|Категория N[...]
}}

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

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

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

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

resolve_country

Функция используется для экспорта в другие модули. Принимает заголовок страницы (или принимает title) + строку. Обрабатывает строку, если в ней содержалась заготовка страны, части света или государства. Возвращает таблицу со значениями:

  • result — основной вариант строки для единственного результата (или первой части света или государства),
  • extra_result — возможный второй вариант строки для второй части света или государства,
  • error — код ошибки (0 — ошибки нет, 1 — страна не найдена, 2 — часть света не найдена).


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

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

См. также

[править код]
local p = {}
local getArgs = require('Модуль:Arguments').getArgs
local findCountry = require('Модуль:Find country')
local error_category = '[[Категория:Википедия:Страницы с некорректным использованием модуля CountryMetaCat]]'

local errors = {}

--------------- Отслеживание ошибок ---------------
-- Добавляет сообщение об ошибке в список с указанием кода ошибки
local function add_error(error_code, additional_info)
	local error_specific = {
		[1] = 'Ошибка: страна не найдена.',
		[2] = 'Ошибка: часть света не найдена для страны ' .. (additional_info or '') .. '.'
	}

	local error_text = error_specific[error_code]
	if error_text then
		table.insert(errors, '<span class="error">' .. error_text .. '</span>')
	end
end

-- Возвращает строку с ошибками и очищает список ошибок
local function get_errors()
	local result = table.concat(errors)
	if result ~= "" then
		result = result .. error_category
	end
	errors = {} -- Очищаем ошибки после получения
	return result
end

---------------- Обработка государств и частей света ----------------
-- Проверяет наличие указанных плейсхолдеров в аргументах
local function has_placeholders(args, placeholders)
	for _, value in pairs(args) do
		if type(value) == "string" then
			for _, placeholder in ipairs(placeholders) do
				if mw.ustring.find(value, placeholder, 1, true) then
					return true
				end
			end
		end
	end
	return false
end

-- Проверяет наличие плейсхолдеров для частей света
local function has_continent_placeholders(args)
	return has_placeholders(args, {'<часть света>', '<части света>', '<в части света>'})
end

-- Проверяет наличие плейсхолдеров для государств
local function has_state_placeholders(args)
	for _, value in pairs(args) do
		if type(value) == "string" then
			if	mw.ustring.find(value, '<государство[^>]*>') or
				mw.ustring.find(value, '<государства[^>]*>') or
				mw.ustring.find(value, '<в государстве[^>]*>') then
				return true
			end
		end
	end
	return false
end

-- Загрузка данных о государствах из JSON
local function load_states_data()
	return mw.loadJsonData('Модуль:CountryMetaCat/state-data.json')
end

-- Проверяет, попадает ли заданный год в указанный период (век, десятилетие, год)
local function check_time_period(start_year, end_year, type, time)
	local year = tonumber(time)
	if not year then return false end

	if type == "century" then
		local century_start = (year - 1) * 100 + 1
		local century_end = year * 100
		return	(not end_year and start_year <= century_end) or
				(start_year <= century_end and end_year and end_year >= century_start)
	elseif type == "decade" then
		local decade_start = math.floor(year / 10) * 10
		local decade_end = decade_start + 9
		return	(not end_year and start_year <= decade_end) or
				(start_year <= decade_end and end_year and end_year >= decade_start)
	else -- year
		return	(not end_year and start_year <= year) or
				(start_year <= year and end_year and end_year >= year)
	end
end

-- Безопасно возвращает название страны в нужном падеже
local function get_safe_case(country, case)
	if not country then return "" end
	local result = findCountry.findcountryinstring(country, case)
	if not result or result:match("^Ошибка") then result = "" end
	return result
end

-- Находит государства по временному периоду, и сортирует их
local function find_states_by_time(country, type, time)
	local states = {}
	local states_data = load_states_data()
	local country_data = states_data.country[country]

	if not country_data then return states end

	-- Создаем массив с годами для сортировки
	local states_with_years = {}
	for state_name, years in pairs(country_data) do
		local start_year = tonumber(years[1])
		local end_year = years[2] and tonumber(years[2]) or nil

		if check_time_period(start_year, end_year, type, time) then
			table.insert(states_with_years, {
				name = state_name,
				start_year = start_year,
				end_year = end_year,
				cases = {
					['именительный'] = get_safe_case(state_name, 'именительный'),
					['родительный'] = get_safe_case(state_name, 'родительный'),
					['предлог'] = get_safe_case(state_name, 'предлог')
				}
			})
		end
	end

	-- Сортируем по начальному году (по возрастанию)
	table.sort(states_with_years, function(a, b)
		return a.start_year < b.start_year
	end)

	-- При запросе со временем возвращаем все подходящие государства для дальнейшей обработки
	return states_with_years
end

-- Проверяет соответствие названия государства из плейсхолдера с указанным государством
local function check_state_match(state, placeholder)
	if not state then return false end

	local required_state_name = mw.ustring.match(placeholder, ":([^>]+)>")
	if not required_state_name then return true end -- если нет конкретного требования, разрешаем любое государство

	-- Нормализуем названия для корректного сравнения
	required_state_name = mw.ustring.gsub(required_state_name, "%s+", " ")
	local state_name = mw.ustring.gsub(state.name or "", "%s+", " ")

	-- Проверяем на исключения (формат ^Страна1^Страна2^...)
	if mw.ustring.match(required_state_name, "^%^") then
		-- Разбиваем строку исключений на отдельные страны
		local excluded_states = {}
		for excluded_state in mw.ustring.gmatch(required_state_name, "%^([^%^]+)") do
			excluded_states[mw.ustring.gsub(excluded_state, "%s+", " ")] = true
		end
		-- Возвращаем true, если государство НЕ в списке исключений
		return not excluded_states[state_name]
	else
		-- Обычное сравнение для включения конкретной страны
		return state_name == required_state_name
	end
end

-- Заменяет плейсхолдеры в тексте на значения из таблицы
local function replace_placeholders(text, replacements)
	for placeholder, value in pairs(replacements) do
		text = mw.ustring.gsub(text, placeholder, value)
	end
	return text
end

-- Удаляет неиспользованные плейсхолдеры из текста
local function remove_unused_placeholders(text, placeholders)
	for _, placeholder in ipairs(placeholders) do
		text = mw.ustring.gsub(text, placeholder, '')
	end
	return text
end

-- Вспомогательная функция для замены плейсхолдера государства в нужном падеже
local function replace_state_placeholder(result, pattern, case, state, is_question)
	return result:gsub(pattern, function(state_name)
		if check_state_match(state, pattern:gsub(":([^>]+)>", ":" .. state_name .. ">")) then
			return state.cases[case] or ""
		end
		return is_question and (pattern:gsub("([^>]+)>", state_name .. ">")) or ""
	end)
end

--------------- Обработка всех плейсхолдеров ---------------
-- Основная обработка всех плейсхолдеров с заменой
local function process_placeholders(s, country, continent, state, is_question)
	if not s then return "" end

	local result = s
	local has_exclamation_space = mw.ustring.find(s, "! ")

	-- Проверка на пустой возврат для не "?"
	if not is_question then
		if not state and result:match('<[^>]*государств[^>]*>') then
			return ""
		end
		if state and result:match('<[^>]+:[^>]+>') then
			local found_match = false
			for pattern in result:gmatch('<[^>]+:[^>]+>') do
				if check_state_match(state, pattern) then
					found_match = true
					break
				end
			end
			if not found_match then return "" end
		end
	end

	-- Обработка плейсхолдеров государства с использованием вспомогательной функции
	if state then
		result = replace_placeholders(result, {
			['<государство>'] = state.cases['именительный'],
			['<государства>'] = state.cases['родительный'],
			['<в государстве>'] = state.cases['предлог']
		})

		result = replace_state_placeholder(result, '<государство:([^>]+)>', 'именительный', state, is_question)
		result = replace_state_placeholder(result, '<государства:([^>]+)>', 'родительный', state, is_question)
		result = replace_state_placeholder(result, '<в государстве:([^>]+)>', 'предлог', state, is_question)
	end

	-- Обработка плейсхолдеров частей света (без изменений)
	if result:match('<[^>]*част[^>]*света[^>]*>') then
		if continent and continent.cases then
			result = replace_placeholders(result, {
				['<часть света>'] = continent.cases['именительный'] or '',
				['<части света>'] = continent.cases['родительный'] or '',
				['<в части света>'] = continent.cases['предложный'] and ('в ' .. continent.cases['предложный']) or ''
			})
		else
			add_error(2, country)
			result = remove_unused_placeholders(result, {
				'<часть света>', '<части света>', '<в части света>'
			})
		end
	end

	-- Обработка плейсхолдеров страны (без изменений)
	if country then
		result = replace_placeholders(result, {
			['<страна>'] = get_safe_case(country, 'именительный') or "",
			['<страны>'] = get_safe_case(country, 'родительный') or "",
			['<в стране>'] = get_safe_case(country, 'предлог') or ""
		})
	elseif not is_question then
		result = remove_unused_placeholders(result, {
			'<страна>', '<страны>', '<в стране>'
		})
	end

	-- Финальная обработка (без изменений)
	result = mw.ustring.gsub(result, " !", "!")
	result = mw.ustring.gsub(result, "[%!%s]+$", "")
	if has_exclamation_space and not mw.ustring.find(result, "!") then
		result = result .. "! "
	end

	return result
end

--------------- Обработка и публикация категорий ---------------
-- Генерирует комбинации частей света и государств
local function combinations_categories(country, args)
	local combinations = {}
	local continents = {}
	local states = {}

	-- Загрузка данных о частях света
	if has_continent_placeholders(args) then
		local continents_data = mw.loadJsonData('Модуль:CountryMetaCat/country-continents.json')
		for _, continent in ipairs(continents_data.continents) do
			if continent.countries then
				for _, c in ipairs(continent.countries) do
					if c == country then
						table.insert(continents, continent)
						break
					end
				end
			end
		end
	end

	-- Получение государств
	if has_state_placeholders(args) then
		states = find_states_by_time(country, args.type or "year", args.time or os.date("%Y"))
	end

	-- Вспомогательная функция для добавления комбинаций
	local function add_combinations(continents_list, states_list)
		if #continents_list == 0 and #states_list == 0 then
			table.insert(combinations, { continent = nil, state = nil })
		else
			for _, continent in ipairs(continents_list) do
				for _, state in ipairs(states_list) do
					table.insert(combinations, { continent = continent, state = state })
				end
			end
			-- Добавляем оставшиеся комбинации, если один из списков пуст
			if #continents_list > 0 and #states_list == 0 then
				for _, continent in ipairs(continents_list) do
					table.insert(combinations, { continent = continent, state = nil })
				end
			elseif #continents_list == 0 and #states_list > 0 then
				for _, state in ipairs(states_list) do
					table.insert(combinations, { continent = nil, state = state })
				end
			end
		end
	end

	-- Добавляем комбинации частей света и государств
	add_combinations(continents, states)

	return combinations
end

-- Проверка существования категории
local function category_exists(category_name)
	if not category_name or category_name == '' then return false end
	local clean_category_name = mw.ustring.match(category_name, "^(.-)!")
	clean_category_name = clean_category_name or category_name
	local title = mw.title.new('Категория:' .. clean_category_name)
	return title and title.exists
end

-- Обрабатывает и добавляет уникальные категории на основе комбинаций частей света и государств
local function process_and_add_categories(text, country, combinations, results, added_categories, check_exists)
	for _, combination in ipairs(combinations) do
		if not combination.state or check_state_match(combination.state, text) then
			local processed = process_placeholders(text, country, combination.continent, combination.state, check_exists)
			if processed ~= "" then
				local category_name = mw.ustring.match(processed, "^(.-)!") or processed
				if not added_categories[category_name] then
					table.insert(results, string.format('[[Категория:%s]]',
						mw.ustring.gsub(processed, "!", "|")))
					added_categories[category_name] = true
				end
			end
		end
	end
end

-- Создание категорий на основе аргументов и комбинаций
local function create_categories(args, country, combinations)
	local results = {}
	local added_categories = {}

	if #combinations == 0 and has_continent_placeholders(args) then
		add_error(2, country)
	end

	local ordered_args = {}
	for i, arg in pairs(args) do
		if type(arg) == "string" and arg ~= "" and type(i) == "number" then
			table.insert(ordered_args, {index = i, value = arg})
		end
	end
	table.sort(ordered_args, function(a, b) return a.index < b.index end)

	local i = 1
	while i <= #ordered_args do
		local arg_data = ordered_args[i]
		local arg = arg_data.value

		if type(arg) == "string" and arg ~= "" then
			local first_char = mw.ustring.sub(arg, 1, 1)
			local rest_string = mw.ustring.sub(arg, 2):gsub("^%s+", "")

			if first_char == '?' then
				local replacements = {}
				local j = i + 1
				while j <= #ordered_args and mw.ustring.sub(ordered_args[j].value, 1, 1) == '~' do
					local replacement_text = mw.ustring.sub(ordered_args[j].value, 2):gsub("^%s+", "")
					table.insert(replacements, replacement_text)
					j = j + 1
				end
				i = j - 1

				-- Обрабатываем каждую комбинацию отдельно
				for _, combination in ipairs(combinations) do
					local processed = process_placeholders(rest_string, country, combination.continent, combination.state, true)
					if processed ~= "" then
						local category_name = mw.ustring.match(processed, "^(.-)!") or processed

						if category_exists(category_name) then
							-- Если категория существует, добавляем её
							if not added_categories[category_name] then
								table.insert(results, string.format('[[Категория:%s]]',
									mw.ustring.gsub(processed, "!", "|")))
								added_categories[category_name] = true
							end
						else
							-- Если категория не существует, обрабатываем замены для этой комбинации
							for _, replacement in ipairs(replacements) do
								local replacement_processed = process_placeholders(replacement, country,
									combination.continent, combination.state, false)
								if replacement_processed ~= "" then
									local replacement_category = mw.ustring.match(replacement_processed, "^(.-)!") or replacement_processed
									if not added_categories[replacement_category] then
										table.insert(results, string.format('[[Категория:%s]]',
											mw.ustring.gsub(replacement_processed, "!", "|")))
										added_categories[replacement_category] = true
									end
								end
							end
						end
					end
				end
			elseif first_char ~= '~' then
				-- Обработка обычных категорий без проверки существования
				process_and_add_categories(arg, country, combinations, results, added_categories, false)
			end
		end
		i = i + 1
	end

	return table.concat(results)
end

-- Основная функция модуля
function p.main(frame)
	local args = getArgs(frame)
	local title = args.title or mw.title.getCurrentTitle().text

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

	local country = findCountry.findcountryinstring(title, 'именительный')
	if not country or country == "" then
		add_error(1)
		return get_errors()
	end

	local combinations = combinations_categories(country, args)

	local result = create_categories(args, country, combinations)

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

	return result .. get_errors()
end

-- Функция для внешней обработки стран
function p._resolve_country(args)
	local title = args.title or mw.title.getCurrentTitle().text
	local country = findCountry.findcountryinstring(title, 'именительный')
	local text = args[1] or ""
	local is_question = text:match("^%?")
	local has_state = text:match('<[^>]*государств[^>]*>')
	local has_specific_state = text:match('<[^>]+:[^>]+>')
	
	local result = {
		result = "",
		extra_result = nil,
		error = country and country ~= "" and 0 or 1
	}
	
	-- Получаем комбинации, если страна найдена
	local combinations = country and country ~= "" and combinations_categories(country, args) or {}
	
	-- Ранний выход, если это ? с названием государства, но комбинаций нет
	if is_question and has_state and #combinations == 0 then
		result.result = process_placeholders(text, country, nil, nil, true)
		return result
	end
	
	-- Проверка на ошибку части света
	if #combinations > 0 and has_continent_placeholders(args) and not combinations[1].continent then
		result.error = 2
	end
	
	-- Сбор допустимых результатов
	local valid_results = {}
	for _, combo in ipairs(combinations) do
		local should_process = is_question and
			(not has_specific_state or (combo.state and check_state_match(combo.state, text))) or
			(not has_state or (combo.state and check_state_match(combo.state, text)))
			
		if should_process then
			local processed = process_placeholders(text, country, combo.continent, combo.state, is_question)
			if processed ~= "" then
				table.insert(valid_results, processed)
				if #valid_results >= 2 then break end -- We only need max 2 results
			end
		end
	end
	
	-- Результаты
	if #valid_results > 0 then
		result.result = valid_results[1]
		result.extra_result = valid_results[2]
	elseif is_question and has_state then
		result.result = process_placeholders(text, country, 
			combinations[1] and combinations[1].continent, nil, true)
	else
		result.result = process_placeholders(text, country, nil, nil, is_question)
	end
	
	return result
end

function p.resolve_country(frame)
	local args = getArgs(frame)
	return p._resolve_country(args)
end

return {
	main = p.main,
	resolve_country = p.resolve_country
}