본문으로 이동

모듈:category tree/topic

위키낱말사전, 말과 글의 누리

이 모듈에 대한 설명문서는 모듈:category tree/topic/설명문서에서 만들 수 있습니다

local raw_handlers = {}
local raw_categories = {}


--[=[
이 모듈은 주제 분류 하위 시스템을 구현합니다.
현재는 언어별 및 포괄 주제 분류를 모두 처리하는 단일 원시 핸들러와,
이에 대응하는 시소러스 분류 핸들러로 구현되어 있습니다.
최상위 주제 분류인 [[:분류:모든 주제]]는 특별하며 별도의 원시 분류로 처리될 수 있지만,
현재는 원시 주제 핸들러의 일부로 처리됩니다. 최상위 시소러스 분류인 [[:분류:시소러스]]는
실제로 원시 분류로 처리됩니다.
]=]

local functions_module = "Module:fun"
local labels_utilities_module = "Module:labels/utilities"
local languages_module = "Module:languages"
local string_pattern_escape_module = "Module:string/patternEscape"
local string_replacement_escape_module = "Module:string/replacementEscape"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local ko_module

local topic_data_module = "Module:category tree/topic/data"
local topic_utilities_module = "Module:category tree/topic/utilities"
local thesaurus_data_module = "Module:category tree/topic/thesaurus data"

local concat = table.concat
local insert = table.insert
local dump = mw.dumpObject
local is_callable = require(functions_module).is_callable
local pattern_escape = require(string_pattern_escape_module)
local replacement_escape = require(string_replacement_escape_module)
local split = require(string_utilities_module).split

local type_data = {
	["related-to"] = {
		desc_func = function(topic)
			ko_module = ko_module or require("Module:ko")
			-- '와/과' 조사를 자동으로 처리합니다.
			return ko_module.allomorphy(topic, "conj") .. " 관련된 낱말"
		end,
		additional = "'''참고''': 이 분류는 '관련' 분류입니다. {{{topic}}}와(과) 직접적으로 관련된 낱말을 포함해야 합니다. " ..
		"단순히 부수적인 연관성만 있는 낱말은 포함하지 마십시오. " ..
		"이 주제의 유형이나 사례에 해당하는 낱말은 종종 별도의 분류에 속한다는 점에 유의하십시오.",
	},
	set = {
		desc_func = function(topic)
			return topic .. "의 종류나 예시에 해당하는 낱말"
		end,
		additional = "'''참고''': 이 분류는 '집합' 분류입니다. {{{topic}}}에 단순히 관련된 낱말이 아닌, {{{topic}}} 자체에 해당하는 낱말을 포함해야 합니다. " ..
		"더 일반적인 낱말(예: {{{topic}}}의 종류)이나 더 구체적인 낱말(예: 특정 {{{topic}}}의 명칭)을 포함할 수 있으나, " ..
		"이러한 유형의 낱말들을 위한 관련 분류가 별도로 존재할 수 있습니다.",
	},
	name = {
		desc_func = function(topic)
			return topic .. "의 구체적인 명칭"
		end,
		additional = "'''참고''': 이 분류는 '명칭' 분류입니다. {{{topic}}}에 관련된 낱말이나 {{{topic}}}의 종류에 대한 일반적인 낱말이 아닌, " ..
		"특정 {{{topic}}}의 명칭을 포함해야 합니다.",
	},
	type = {
		desc_func = function(topic)
			return topic .. "의 종류에 해당하는 낱말"
		end,
		additional = "'''참고''': 이 분류는 '유형' 분류입니다. {{{topic}}}에 관련된 낱말이나 특정 {{{topic}}}의 명칭이 아닌, " ..
		"{{{topic}}}의 유형에 해당하는 낱말을 포함해야 합니다.",
	},
	grouping = {
		desc_func = function(topic)
			return topic .. "의 더 구체적인 변종에 관한 분류"
		end,
		additional = "'''참고''': 이 분류는 '그룹' 분류입니다. 어떠한 낱말도 직접 포함해서는 안 되며, " ..
		"오직 하위 분류만을 포함해야 합니다. 만약 이 분류에 직접 포함된 낱말이 있다면, 적절한 하위 분류로 옮겨주십시오.",
	},
	toplevel = {
		desc_func = function(topic) return "" end, -- 이 유형은 설명을 직접 지정함
		additional = "'''참고''': 이 분류는 최상위 목록 분류입니다. 어떠한 낱말도 직접 포함해서는 안 되며, " ..
		"오직 {{{topic}}}만을 포함해야 합니다.",
	},
}

local function invalid_type(types)
	local valid_types = {}
	for typ, _ in pairs(type_data) do
		insert(valid_types, ("'%s'"):format(typ))
	end
	error(("유효하지 않은 유형 '%s'. 쉼표로 구분된 다음 값 중 하나여야 합니다: %s")
		:format(types, mw.text.listToText(valid_types)))
end

local function split_types(types)
	types = types or "related-to"
	local splitvals = split(types, "%s*,%s*")
	for i, typ in ipairs(splitvals) do
		-- FIXME: 임시 조치
		if typ == "topic" then
			typ = "related-to"
		end
		if not type_data[typ] then
			invalid_type(types)
		end
		splitvals[i] = typ
	end
	return splitvals
end


local function gsub_escaping_replacement(str, from, to)
	return (str:gsub(pattern_escape(from), replacement_escape(to)))
end


function ucfirst(txt)
	local italics, raw_txt = txt:match("^('*)(.-)$")
	return italics .. mw.getContentLanguage():ucfirst(raw_txt)
end


function lcfirst(txt)
	local italics, raw_txt = txt:match("^('*)(.-)$")
	return italics .. mw.getContentLanguage():lcfirst(raw_txt)
end


function convert_spec_to_string(data, desc)
	if not desc then
		return desc
	end
	local desc_type = type(desc)
	if desc_type == "string" then
		return desc
	elseif desc_type == "number" then
		return tostring(desc)
	elseif not is_callable(desc) then
		error("내부 오류: `desc`는 문자열, 숫자, 함수, 호출 가능한 테이블 또는 nil이어야 하나, " ..
			desc_type .. " 유형이 전달되었습니다.")
	end
	desc = desc {
		lang = data.lang,
		sc = data.sc,
		label = data.label,
		category = data.category,
		topdata = data.topdata,
	}
	if not desc then
		return desc
	end
	desc_type = type(desc)
	if desc_type == "string" then
		return desc
	end
	error("내부 오류: `desc` 함수가 반환하는 값은 문자열 또는 nil이어야 하나, " .. desc_type .. " 유형이 반환되었습니다.")
end

local function get_and_cache(data, obj, key)
	local val = convert_spec_to_string(data, obj[key])
	obj[key] = val
	return val
end

local function process_default(desc)
	local stripped_desc = desc
	local no_singularize, wikify, add_the
	while true do
		local new_stripped_desc = stripped_desc:match("^(.+) no singularize$")
		if new_stripped_desc then
			no_singularize = true
		end
		if not new_stripped_desc then
			new_stripped_desc = stripped_desc:match("^(.+) wikify$")
			if new_stripped_desc then
				wikify = true
			end
		end
		if not new_stripped_desc then
			new_stripped_desc = stripped_desc:match("^(.+) with the$")
			if new_stripped_desc then
				add_the = true
			end
		end
		if new_stripped_desc then
			stripped_desc = new_stripped_desc
		else
			break
		end
	end
	if stripped_desc == "default" then
		return true, no_singularize, wikify, add_the
	else
		return false
	end
end

local function format_desc(data, topic_word)
	local desc_parts = {}
	local types = split_types(data.topdata.type)
	for _, typ in ipairs(types) do
		-- 각 유형에 정의된 desc_func 함수를 호출하여 설명문을 생성합니다.
		insert(desc_parts, type_data[typ].desc_func(topic_word))
	end
	return "{{{langname}}} " .. require("Module:table").serialCommaJoin(desc_parts) .. "."
end

local substitute_template_specs

local function format_displaytitle(data, include_lang_prefix, upcase)
	local topdata, lang, label = data.topdata, data.lang, data.label
	local displaytitle = substitute_template_specs(data, topdata.displaytitle)
	if not displaytitle then
		return nil
	end
	if upcase then
		displaytitle = ucfirst(displaytitle)
	end
	if include_lang_prefix and lang then
		displaytitle = ("%s:%s"):format(lang:getCode(), displaytitle)
	end

	return displaytitle
end

local function get_breadcrumb(data)
	local topdata, lang, label = data.topdata, data.lang, data.label
	local ret

	if lang then
		ret = topdata.breadcrumb or format_displaytitle(data, false, "upcase")
	else
		ret = topdata.umbrella and topdata.umbrella.breadcrumb or
			topdata.breadcrumb or format_displaytitle(data, false, "upcase")
	end
	if not ret then
		ret = label
	end

	if type(ret) == "string" or type(ret) == "number" then
		ret = {name = ret}
	end

	local name = substitute_template_specs(data, ret.name)
	local nocap = ret.nocap

	return {name = name, nocap = nocap}
end

local function make_category_name(lang, label)
	if lang then
		return lang:getCode() .. ":" .. ucfirst(label)
	else
		return ucfirst(label)
	end
end

local function replace_special_descriptions(data, desc)
	if not desc then
		return desc
	end

	if desc:find("^=") then
		desc = desc:gsub("^=", "")
		return format_desc(data, desc)
	end

	local is_default, no_singularize, wikify, add_the = process_default(desc)
	if is_default then
		local linked_label = require(topic_utilities_module).link_label(data.label, no_singularize, wikify)
		if add_the then
			linked_label = "the " .. linked_label
		end
		return format_desc(data, linked_label)
	else
		return desc
	end
end


local function get_displaytitle_or_label(data)
	return format_displaytitle(data, false) or data.label
end


local function process_default_add_the(data, topic)
	local is_default, _, _, add_the = process_default(topic)
	if is_default then
		topic = get_displaytitle_or_label(data)
		if add_the then
			topic = "the " .. topic
		end
	end
	return topic, is_default
end


substitute_template_specs = function(data, desc)
	desc = convert_spec_to_string(data, desc)
	if not desc then
		return nil
	end

	local topdata, lang, label = data.topdata, data.lang, data.label
	if desc:find("{{{umbrella_msg}}}") then
		local catname = ucfirst(label)
		desc = gsub_escaping_replacement(desc, "{{{umbrella_msg}}}",
			"이 분류는 사전 항목을 직접 포함하지 않고 다른 분류만을 포함합니다. 하위 분류는 두 종류로 나뉩니다:\n\n" ..
			"* \"{{{thespref}}}" .. "aa:" .. catname ..
			"\"처럼 언어 코드가 앞에 붙는 하위 분류들은 특정 언어의 낱말들을 담는 분류입니다. " ..
			"특히 [[:분류:{{{thespref}}}ko:" .. catname .. "]]에서 한국어 낱말을 찾아보실 수 있습니다.\n" ..
			"* 언어 코드가 붙지 않는 하위 분류들은 이 분류와 마찬가지로 더 세부적인 주제를 다루는 분류들입니다."
		)
	end
	if desc:find("{{{topic}}}") then
		-- {{{topic}}}의 값을 계산합니다. 사용자가 `topic`을 지정하면 그 값을 사용합니다.
		-- (포괄 분류인 경우 `umbrella.topic` 값을 별도로 허용하며, 없으면 `topic`으로 대체).
		-- 그렇지 않으면, 설명이 'default'로 지정되었는지 확인하여 'the'를 붙일지 결정합니다.
		-- 그 외의 경우, 레이블을 직접 사용합니다.
		local topic = not lang and topdata.umbrella and topdata.umbrella.topic or topdata.topic
		if topic then
			topic = process_default_add_the(data, topic)
		else
			local desc_val
			if not lang then
				desc_val = topdata.umbrella and get_and_cache(data, topdata.umbrella, "description") or
					get_and_cache(data, topdata, "umbrella_description")
			end
			desc_val = desc_val or get_and_cache(data, topdata, "description")
			local defaulted_desc, is_default = process_default_add_the(data, desc_val)
			if is_default then
				topic = defaulted_desc
			else
				topic = get_displaytitle_or_label(data)
			end
		end
		
		-- '와(과)' 조사가 필요한 경우를 감지하고 allomorphy 함수를 적용합니다.
		desc = desc:gsub("{{{topic}}}와%(과%)", function()
			ko_module = ko_module or require("Module:ko")
			return ko_module.allomorphy(topic, "conj")
		end)
		
		-- 나머지 일반적인 경우는 그대로 치환합니다.
		desc = gsub_escaping_replacement(desc, "{{{topic}}}", topic)
	end
	
	-- {{{thespref}}}를 시소러스 접두사로 치환합니다.
	desc = desc:gsub("{{{thespref}}}", data.thesaurus_data and "시소러스:" or "")
	return desc
end

local function process_box(data, def_topright_parts, val, pattern)
	if not val then
		return
	end
	local defval = ucfirst(data.label)
	if type(val) ~= "table" then
		val = {val}
	end
	for _, v in ipairs(val) do
		if v == true then
			insert(def_topright_parts, pattern:format(defval))
		else
			insert(def_topright_parts, pattern:format(v))
		end
	end
end

local function get_topright(data)
	local topdata, lang = data.topdata, data.lang
	local def_topright_parts = {}
	-- {{wikipedia}}, {{commonscat}} 틀이 한국어 위키낱말사전에 존재해야 합니다.
	process_box(data, def_topright_parts, topdata.wp, "{{위키백과|%s}}")
	process_box(data, def_topright_parts, topdata.wpcat, "{{위키백과|분류=%s}}")
	process_box(data, def_topright_parts, topdata.commonscat, "{{위키공용분류|%s}}")
	local def_topright
	if #def_topright_parts > 0 then
		def_topright = concat(def_topright_parts, "\n")
	end
	if lang then
		return substitute_template_specs(data, topdata.topright or def_topright)
	else
		return topdata.umbrella and substitute_template_specs(data, topdata.umbrella.topright) or
			substitute_template_specs(data, def_topright)
	end
end


local function remove_lang_params(desc)
	desc = desc:gsub("^{{{langname}}} ", "")
	desc = desc:gsub("{{{langcode}}}:", "")
	desc = desc:gsub("^{{{langcode}}} ", "")
	desc = desc:gsub("^{{{langcat}}} ", "")
	return desc
end


local function get_additional_msg(data)
	local types = split_types(data.topdata.type)
	if #types > 1 then
		local parts = {"'''참고''': 이것은 복합 유형 분류입니다. 다음 분류 유형에 해당하는 모든 낱말을 포함할 수 있습니다:"}
		for i, typ in ipairs(types) do
			insert(parts, ("* {{{topic}}}%s %s"):format(type_data[typ].desc_func and "" or " ", type_data[typ].desc_func and type_data[typ].desc_func(topic) or type_data[typ].desc, i == #types and "." or ";"))
		end
		insert(parts, "'''경고''': 이러한 분류는 강력히 지양되며, 각 유형별 분류로 분리되어야 합니다.")
		return concat(parts, "\n")
	elseif data.label == "모든 주제" then
		return "'''참고''': 이것은 {{{langname}}}의 최상위 주제 분류입니다. 어떠한 낱말도 직접 포함해서는 안 되며, " ..
		"유형별로 정리된 주제 분류 목록만을 포함해야 합니다."
	else
		return type_data[types[1]].additional
	end
end


local function get_labels_categorizing(data)
	local m_labels_utilities = require(labels_utilities_module)
	return m_labels_utilities.format_labels_categorizing(
		m_labels_utilities.find_labels_for_category(data.label, "topic", data.lang), nil, data.lang)
end


-- 설명, 그리고 설명 앞뒤에 오는 텍스트를 반환하는 함수.
-- 실제 텍스트 계산 작업(비용이 높을 수 있음)이 필요할 때만 수행되도록 클로저(closure) 형태로 반환.
local function get_description_additional_preceding(data)
	local topdata, lang, label = data.topdata, data.lang, data.label
	local desc, additional, preceding

	-- This is kind of hacky, but it works for now.
	local function postprocess_thesaurus(txt)
		if not txt then return nil end
		if not data.thesaurus_data then return txt end
		txt = txt:gsub(" 낱말([ .,])", " 시소러스 항목%1")
		return txt
	end

	if lang then
		desc = function()
			return postprocess_thesaurus(substitute_template_specs(data,
				replace_special_descriptions(data, get_and_cache(data, topdata, "description"))))
		end
		preceding = topdata.preceding
		additional = function()
			local additional_parts = {}
			if topdata.additional then
				insert(additional_parts, topdata.additional)
			end
			if not data.thesaurus_data then
				insert(additional_parts, get_additional_msg(data))
				local labels_msg = get_labels_categorizing(data)
				if labels_msg then
					insert(additional_parts, labels_msg)
				end
			end
			return postprocess_thesaurus(substitute_template_specs(data, concat(additional_parts, "\n\n")))
		end
	else
		if label == "모든 주제" then
			desc = "모든 언어에 대한 최상위 주제 분류입니다."
			additional = "사전 항목을 직접 포함하지 않고 다른 분류만을 포함합니다. 하위 분류는 두 종류로 나뉩니다:\n\n" ..
				"* 맨 처음에 나열된, 언어 코드가 없는 하위 분류들은 이 분류와 유사한 그룹 분류로, 일반적인 주제 영역을 다룹니다. 그 아래에 더 세분화된 주제 영역이 있습니다.\n" ..
				"* \"ko:모든 주제\"와 같이 언어 코드가 앞에 붙는 하위 분류들은 이 분류와 같지만 특정 언어에 대한 최상위 분류입니다. 특히 " ..
				"[[:분류:ko:모든 주제]]에서 한국어 관련 주제를 찾아보실 수 있습니다.\n" ..
				"참고로 이 분류 체계 아래의 분류들은 문법적이 아닌 의미적으로 낱말을 분류합니다. " ..
				"문법적 분류(예: 모든 프랑스어 동사)는 [[:분류:프랑스어 동사]]와 같이 언어 이름 전체를 사용하는 다른 명명 규칙을 가집니다."
			return desc, additional
		end

		local has_umbrella_desc = topdata.umbrella and topdata.umbrella.description or topdata.umbrella_description

		desc = function()
			local desc = topdata.umbrella and get_and_cache(data, topdata.umbrella, "description") or
				get_and_cache(data, topdata, "umbrella_description")
			if not desc then
				 desc = get_and_cache(data, topdata, "description")
				 if desc then
					desc = replace_special_descriptions(data, desc)
					desc = remove_lang_params(desc)
					desc = desc:gsub("%.$", "")
					desc = "이 분류는 " .. desc .. " 주제와 관련이 있습니다."
				 end
			end
			if not desc then
				desc = "다양한 특정 언어의 " .. label .. " 관련 분류입니다."
			end
			return postprocess_thesaurus(substitute_template_specs(data, desc))
		end

		preceding = topdata.umbrella and topdata.umbrella.preceding or not has_umbrella_desc and topdata.preceding
		if preceding then
			preceding = remove_lang_params(preceding)
		end

		additional = function()
			local additional_parts = {}
			local topdata_additional = topdata.umbrella and topdata.umbrella.additional or
				not has_umbrella_desc and topdata.additional
			if topdata_additional then
				insert(additional_parts, remove_lang_params(topdata_additional))
			end
			insert(additional_parts, "{{{umbrella_msg}}}")
			if not data.thesaurus_data then
				insert(additional_parts, get_additional_msg(data))
				local labels_msg = get_labels_categorizing(data)
				if labels_msg then
					insert(additional_parts, labels_msg)
				end
			end
			return postprocess_thesaurus(substitute_template_specs(data, concat(additional_parts, "\n\n")))
		end
	end

	preceding = substitute_template_specs(data, preceding)
	return desc, additional, preceding
end


local function normalize_sort_key(data, sort)
	local lang, label = data.lang, data.label
	
	if not sort then
		-- 정렬 키 기본값을 레이블로 할 때, 영어 관사 'The' 또는 'A'를 제거.
		local stripped_sort = label:match("^[Tt]he (.*)$")
		if stripped_sort then sort = stripped_sort end
		if not stripped_sort then
			stripped_sort = label:match("^[Aa] (.*)$")
			if stripped_sort then sort = stripped_sort end
		end
		if not stripped_sort then sort = label end
	end
	
	sort = substitute_template_specs(data, sort)
	
	if not lang then
		sort = " " .. sort
	end
	return sort
end

local function get_topic_parents(data)
	local topdata, lang, label = data.topdata, data.lang, data.label
	local parents = topdata.parents

	if not lang and label == "모든 주제" then
		return {{ name = "분류:기본", sort = "주제" }}
	end

	if not parents or #parents == 0 then
		return nil
	end

	local ret = {}

	for _, parent in ipairs(parents) do
		parent = mw.clone(parent)
		if type(parent) ~= "table" then
			parent = {name = parent}
		end
		parent.sort = normalize_sort_key(data, parent.sort)
		if type(parent.name) ~= "string" then
			error(("내부 오류: parent.name이 문자열이 아닙니다: parent = %s"):format(dump(parent)))
		end
		if parent.name:find("^분류:") or parent.nontopic then
			-- 그대로 둠
			parent.nontopic = nil
		else
			parent.name = make_category_name(lang, parent.name)
		end
		parent.name = substitute_template_specs(data, parent.name)
		insert(ret, parent)
	end

	local function make_list_of_type_parent(typ_en)
		local typ_ko
		if typ_en == "related-to" then typ_ko = "관련"
		elseif typ_en == "set" then typ_ko = "집합"
		elseif typ_en == "type" then typ_ko = "유형"
		elseif typ_en == "name" then typ_ko = "명칭"
		elseif typ_en == "grouping" then typ_ko = "그룹"
		else typ_ko = typ_en end
		
		return {
			name = make_category_name(lang, typ_ko .. " 분류 목록"),
			sort = (not lang and " " or "") .. label,
		}
	end

	if topdata.type ~= "toplevel" then
		local types = split_types(topdata.type)
		for _, typ in ipairs(types) do
			insert(ret, make_list_of_type_parent(typ))
		end
		if #types > 1 then
			insert(ret, make_list_of_type_parent("복합"))
		end
	end

	-- 포괄 분류를 추가.
	if lang then
		insert(ret, {
			name = make_category_name(nil, label),
			sort = lang:getCanonicalName(),
		})
	end

	return ret
end


local function get_thesaurus_parents(data)
	local topdata, lang, label = data.topdata, data.lang, data.label
	local parent_substitutions = data.thesaurus_data.parent_substitutions
	local parents = topdata.parents

	if not parents or #parents == 0 then
		return nil
	end

	local ret = {}

	for _, parent in ipairs(parents) do
		-- Process parent categories as follows:
		-- 1. skip non-topic cats and meta-categories that start with "List of"
		-- 2. map "en:All topics" to "English thesaurus entries" (and same for other languages), but map "All topics" itself to the root "Thesaurus" category
		-- 3. check if this parent is to be substituted, if so, substitute it
		-- 4. prepend "Thesaurus:" to all other category names
		parent = mw.clone(parent)

		if type(parent) ~= "table" then
			parent = {name = parent}
		end

		parent.sort = normalize_sort_key(data, parent.sort)

		if type(parent.name) ~= "string" then
			error(("내부 오류: parent.name이 문자열이 아닙니다: parent = %s"):format(dump(parent)))
		end
		if parent.name:find("^분류:") or parent.nontopic then
			-- 건너뜀
		elseif parent.name == "모든 주제" or parent_substitutions[parent.name] == "모든 주제" then
			if not lang then
				insert(ret, { name = "시소러스", sort = label })
			else
				insert(ret, { name = "시소러스 항목", sort = parent.sort, lang = lang:getCode(), is_label = true })
			end
		else
			parent.name = "시소러스:" .. make_category_name(lang, parent_substitutions[parent.name] or parent.name)
			parent.name = substitute_template_specs(data, parent.name)
			insert(ret, parent)
		end
	end

	-- 시소러스 전용 분류가 아니라면, 비-시소러스 버전의 분류를 상위 분류로 추가.
	if not topdata.thesaurusonly then
		insert(ret, { name = make_category_name(lang, label), sort = " " })
	end

	-- 포괄 분류를 추가.
	if lang then
		insert(ret, {
			name = "시소러스:" .. make_category_name(nil, label),
			sort = lang:getCanonicalName(),
		})
	end

	return ret
end


local function generate_spec(category, lang, upcase_label, thesaurus_data)
	local label_data = require(topic_data_module)
	local label

	-- Convert label to lowercase if possible
	local lowercase_label = mw.getContentLanguage():lcfirst(upcase_label)

	-- Check if the label exists
	local labels = label_data["LABELS"]

	if labels[lowercase_label] then
		label = lowercase_label
	else
		label = upcase_label
	end

	local topdata = labels[label]

	-- Go through handlers
	if not topdata then
		for _, handler in ipairs(label_data["HANDLERS"]) do
			topdata = handler.handler(label)
			if topdata then
				topdata.module = handler.module
				break
			end
		end
	end

	if not topdata then
		return nil
	end

	local data = {
		category = category,
		lang = lang,
		label = label,
		topdata = topdata,
		thesaurus_data = thesaurus_data,
	}

	local description, additional, preceding = get_description_additional_preceding(data)
	local parents
	if thesaurus_data then
		parents = get_thesaurus_parents(data)
	else
		parents = get_topic_parents(data)
	end

	return {
		lang = lang and lang:getCode() or nil,
		description = description,
		additional = additional,
		preceding = preceding,
		parents = parents,
		breadcrumb = get_breadcrumb(data),
		displaytitle = format_displaytitle(data, "include lang prefix", "upcase"),
		topright = get_topright(data),
		module = topdata.module,
		can_be_empty = not lang,
		hidden = false,
	}
end


-- '시소러스:...' 분류를 위한 핸들러.
table.insert(raw_handlers, function(data)
	local code, upcase_label = data.category:match("^시소러스:(%l[%a-]*%a):(.+)$")
	local lang
	if code then
		lang = require(languages_module).getByCode(code)
		if not lang then
			mw.log( ("분류 '%s'는(은) 언어별 시소러스 분류처럼 보이지만 언어 접두사와 일치시킬 수 없습니다."):format(data.category) )
			return nil
		end
	else
		upcase_label = data.category:match("^시소러스:(.+)$")
	end

	if upcase_label then
		local thesaurus_data = require(thesaurus_data_module)
		if thesaurus_data.parent_substitutions[lcfirst(upcase_label)] then
			error(("이 분류는 시소러스 분류로 허용되지 않습니다: %s (자세한 내용은 [[모듈:category tree/topic/thesaurus]]의 상위 분류 치환 목록 참조)")
				:format(data.category))
		end
		return generate_spec(data.category, lang, upcase_label, thesaurus_data)
	end
end)

-- 일반 주제 분류를 위한 핸들러.
table.insert(raw_handlers, function(data)
	local code, upcase_label = data.category:match("^(%l[%a-]*%a):(.+)$")
	local lang
	if code then
		lang = require(languages_module).getByCode(code)
		if not lang then
			mw.log( ("분류 '%s'는(은) 언어별 주제 분류처럼 보이지만 언어 접두사와 일치시킬 수 없습니다."):format(data.category) )
			return nil
		end
	else
		upcase_label = data.category
	end
	return generate_spec(data.category, lang, upcase_label)
end)


-----------------------------------------------------------------------------
--                                                                         --
--                              원시(RAW) 분류                             --
--                                                                         --
-----------------------------------------------------------------------------


raw_categories["Thesaurus"] = {
	description = "별도의 이름공간에 위치한 위키낱말사전 시소러스 항목들을 위한 분류입니다.",
	additional = [=[시소러스를 탐색하는 방법에는 '''세 가지'''가 있습니다:
* '''[[:분류:언어별 시소러스 항목]]'''에서 시작하세요.
* 아래 검색 상자를 이용하세요.
* 아래 "하위 분류"의 링크를 사용하여 주제별로 시소러스를 탐색하세요.

주요 프로젝트 문서는 [[위키낱말사전:시소러스]]입니다.
{{ws header|<nowiki/>|link=}}]=],
	parents = {
		"분류:기본",
		"분류:위키낱말사전 프로젝트",
	},
}

return {RAW_CATEGORIES = raw_categories, RAW_HANDLERS = raw_handlers}