본문으로 이동

모듈:affix

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

하위 모듈

[편집]

local export = {}

local debug_force_cat = false -- if set to true, always display categories even on userspace pages

local m_links = require("Module:also/link")
local m_str_utils = require("Module:string utilities")
local m_table = require("Module:table")
local etymology_module = "Module:etymology"
local pron_qualifier_module = "Module:pron qualifier"
local scripts_module = "Module:scripts"
local utilities_module = "Module:utilities"

local m_ko_utils = require("Module:ko-utilities")

-- [[Module:category tree/etymology]]에서 접근할 수 있도록 export
export.affix_lang_data_module_prefix = "Module:affix/lang-data/"

local rsub = m_str_utils.gsub
local usub = m_str_utils.sub
local ulen = m_str_utils.len
local rfind = m_str_utils.find
local rmatch = m_str_utils.match
local u = m_str_utils.char
local ucfirst = m_str_utils.ucfirst
local unpack = unpack or table.unpack -- Lua 5.2 호환성

-- [[Module:category tree/etymology]]에서 접근할 수 있도록 export
export.langs_with_lang_specific_data = {
	["az"] = true,
	["fi"] = true,
	["izh"] = true,
	["la"] = true,
	["sah"] = true,
	["tr"] = true,
}

local default_pos = "낱말"

--[==[ 소개:
=== 다른 종류의 하이픈("틀", "표시", "조회")에 대하여: ===

* "틀 하이픈"은 틀 호출에서 어떤 항이 접사임을 나타내기 위해 사용되는 스크립트별 하이픈 문자입니다.
  항상 단일 유니코드 문자이지만, 특정 스크립트에 대해서는 여러 하이픈이 가능할 수 있습니다.
  보통은 일반 하이픈 문자 "-"이지만, 일부 라틴 문자가 아닌 언어(현재는 오른쪽에서 왼쪽으로 쓰는 언어만 해당)에서는 다릅니다.

* "표시 하이픈"은 어떤 항이 접사임을 나타내기 위해 표시되고 링크될 때 해당 항에 추가되는 문자열입니다 (빈 문자열일 수도 있음).
  현재는 항상 틀 하이픈과 같거나 빈 문자열이지만, 아래 코드는 임의의 표시 하이픈을 처리할 수 있을 만큼 일반적입니다. 구체적으로:
  *# 동아시아 언어의 경우, 표시 하이픈은 항상 비어 있습니다.
  *# 아랍 문자 언어의 경우, 타트윌(ـ) 또는 ZWNJ(폭 없는 비연결자)가 틀 하이픈으로 허용됩니다. ZWNJ는 일부 접미사가
     비연결 동작을 보이기 때문에 주로 페르시아어에서 지원됩니다. 타트윌에 해당하는 표시 하이픈은 역시 타트윌이지만,
     ZWNJ에 해당하는 표시 하이픈은 비어 있습니다 (명시적 하이픈이 없는 {{접두사}}/{{접미사}} 등의 호출에서는 타트윌이 기본 표시 하이픈임).
* "조회 하이픈"은 언어별 접사 매핑을 조회할 때 사용되는 하이픈입니다. (이 매핑은 아래 링크 접사를 논할 때 더 자세히 설명).
  이는 해당 접사의 스크립트에만 의존합니다. 대부분의 스크립트(동아시아 스크립트 포함)는 일반 하이픈 "-"을 조회 하이픈으로 사용하지만,
  히브리어와 아랍어는 각각 고유의 조회 하이픈(마켑과 타트윌)을 가집니다. 특히 아랍어의 경우, 세 가지 가능한 틀 하이픈
  (타트윌, ZWNJ, 일반 하이픈)이 인식되지만, 매핑에서는 반드시 타트윌을 사용해야 합니다.

=== 다른 종류의 접사("틀", "표시", "링크", "조회", "분류")에 대하여: ===

* "틀 접사"는 틀 호출에 나타나는 소스 형태의 접사입니다. 일반적으로 틀 접사에는 틀 하이픈이 붙어 접사임을 나타내고
  어떤 종류의 접사인지(접두사, 접미사, 접요사, 양접사)를 나타내지만, {{접미사}}, {{접두사}} 등 구식 틀 중 일부는
  "위치적" 접사를 가지며, 특정 위치(예: 두 번째 또는 세 번째 매개변수)에 접사가 존재한다는 사실 자체가 틀 하이픈의
  존재 여부와 관계없이 특정 종류의 접사임을 나타냅니다.
* "표시 접사"는 사용자에게 실제로 표시되는 해당 접사입니다. 표시 접사는 여러 이유로 틀 접사와 다를 수 있습니다:
  *# 표시 접사는 `|altN=` 매개변수, `<alt:...>` 인라인 수정자 또는 `[[-kas|-käs]]` 형태의 파이프 링크를 사용하여
     명시적으로 지정될 수 있습니다 (여기서는 접사가 -käs로 표시되지만 -kas로 링크되어야 함을 나타냄).
     이때 틀 접사는 전체 파이프 링크이고, 표시 접사는 -käs입니다.
  *# 위와 같은 명시적 지정이 없더라도, 특정 언어는 틀에 지정된 "틀 하이픈"과 "표시 하이픈"이 다를 수 있으며,
     이에 따라 틀 접사와 표시 접사도 달라집니다.
* (일반) "링크 접사"는 접사가 사용자에게 보일 때 링크되는 접사입니다. 링크 접사는 보통 표시 접사와 같지만,
  세 가지 경우에 달라집니다:
  *# 위 "표시 접사"에서 설명한 대로 명시적 지정으로 표시 접사와 링크 접사가 달라지는 경우.
  *# 특정 언어의 경우, 특정 접사가 언어별 매핑을 통해 표준형으로 변환됩니다. 예를 들어, 핀란드어에서 형용사 형성
     접미사 [[-kas]]는 전설모음 뒤에서 [[-käs]]로 나타나지만, 논리적으로 두 형태는 동일한 접미사이므로 동일하게
     링크되고 분류되어야 합니다. 매핑은 `모듈:affix/lang-data/언어코드`에 제공되어 핀란드어 [[-käs]]를
     링크 및 분류 목적으로 [[-kas]]로 변환합니다. 매핑에 있는 접사들은 "조회 하이픈"을 사용하는데, 이는 보통
     틀 하이픈과 같지만 아랍 문자에서는 다릅니다.
* "정리된 링크 접사"는 언어의 `makeEntryName()` 함수를 거친 링크 접사로, 특정 발음 구별 기호를 제거할 수 있습니다.
  정리된 링크 접사는 현재 분류명에 사용되는 것입니다.
* "조회 접사"는 위에서 설명한 언어별 조회 매핑에서 조회되는 형태의 접사입니다. 실제로는 두 단계의 조회가 있습니다:
  *# 먼저, 수정된 표시 형태(표시 접사와 같지만 조회 하이픈 사용)로 접사를 조회합니다.
  *# 항목을 찾지 못하면, 발음 구별 기호를 제거한 수정된 링크 형태로 다시 조회합니다.
  이 이중 조회 절차는 발음 구별 기호에 민감한 매핑과 그렇지 않은 매핑을 모두 허용하기 위함입니다.
* "분류 접사"는 [[:분류:-kas가 붙은 핀란드어 낱말]]과 같은 분류에 나타나는 접사입니다.
  분류 접사는 현재 항상 정리된 링크 접사와 동일합니다.
]==]

-----------------------------------------------------------------------------------------
--                               Template and display hyphens                          --
-----------------------------------------------------------------------------------------

--[=[
스크립트별 틀 하이픈. 틀 하이픈은 {{접사}}/{{접두사}}/{{접미사}} 등 틀의 위키코드에 나타나는 것입니다. 위 설명 참조.
아래 키는 스크립트 코드에서 하이픈과 그 앞부분을 제거한 것입니다. 따라서 'fa-Arab', 'ur-Arab' 같은 스크립트 코드는 'Arab'과 일치합니다.
값은 하나 이상의 하이픈 문자로 이루어진 문자열입니다.
]=]

local ZWNJ = u(0x200C) -- 폭 없는 비연결자(Zero-width non-joiner)
local template_hyphens = {
	-- This covers all Arabic scripts. See above.
	["Arab"] = "ـ" .. ZWNJ .. "-", -- 타트윌 + ZWNJ + 일반 하이픈
	["Hebr"] = "־", -- 히브리어 고유 하이픈 "마켑(maqqef)"
	["Mong"] = "᠊",
	-- FIXME! 아래 오른쪽-왼쪽 스크립트들에 대한 처리 필요
	-- Adlm (Adlam)
	-- Armi (Imperial Aramaic)
	-- Avst (Avestan)
	-- Cprt (Cypriot)
	-- Khar (Kharoshthi)
	-- Mand (Mandaic/Mandaean)
	-- Mani (Manichaean)
	-- Mend (Mende/Mende Kikakui)
	-- Narb (Old North Arabian)
	-- Nbat (Nabataean/Nabatean)
	-- Nkoo (N'Ko)
	-- Orkh (Orkhon runes)
	-- Phli (Inscriptional Pahlavi)
	-- Phlp (Psalter Pahlavi)
	-- Phlv (Book Pahlavi)
	-- Phnx (Phoenician)
	-- Prti (Inscriptional Parthian)
	-- Rohg (Hanifi Rohingya)
	-- Samr (Samaritan)
	-- Sarb (Old South Arabian)
	-- Sogd (Sogdian)
	-- Sogo (Old Sogdian)
	-- Syrc (Syriac)
	-- Thaa (Thaana)
}

-- 언어별 접사 매핑에서 접사를 조회할 때 사용되는 하이픈. 기본값은 일반 하이픈(-).
-- 키는 스크립트 코드. 값은 단일 문자여야 함.
local lookup_hyphens = {
	["Hebr"] = "־",
	-- 모든 아랍계 스크립트에 해당.
	["Arab"] = "ـ",
}

-- 기본 표시-하이픈 함수
local function default_display_hyphen(script, hyph)
	if not hyph then
		return template_hyphens[script] or "-"
	end
	return hyph
end

-- 아랍계 스크립트의 표시-하이픈 함수
local function arab_get_display_hyphen(script, hyph)
	if not hyph then
		return "ـ" -- 타트윌
	elseif hyph == ZWNJ then
		return ""
	else
		return hyph
	end
end

local function no_display_hyphen(script, hyph)
	return ""
end

-- 스크립트별로 올바른 표시 하이픈을 반환하는 함수
-- 키는 스크립트 코드
local display_hyphens = {
	-- 모든 아랍계 스크립트에 해당
	["Arab"] = arab_get_display_hyphen,
	["Bopo"] = no_display_hyphen, -- 주음부호
	["Hani"] = no_display_hyphen, -- 한자(통합)
	["Hans"] = no_display_hyphen, -- 한자(간체)
	["Hant"] = no_display_hyphen, -- 한자(정체)
	["Jpan"] = no_display_hyphen, -- 일본어
	["Jurc"] = no_display_hyphen, -- 여진 문자
	["Kitl"] = no_display_hyphen, -- 거란 소자
	["Kits"] = no_display_hyphen, -- 거란 대자
	["Laoo"] = no_display_hyphen, -- 라오 문자
	["Nshu"] = no_display_hyphen, -- 여서 문자
	["Shui"] = no_display_hyphen, -- 수 문자
	["Tang"] = no_display_hyphen, -- 서하 문자
	["Thaa"] = no_display_hyphen, -- 타나 문자
	["Thai"] = no_display_hyphen, -- 타이 문자
}

-----------------------------------------------------------------------------------------
--                                 Basic Utility functions                             --
-----------------------------------------------------------------------------------------

local function glossary_link(entry, text)
	text = text or entry
	return "[[부록:용어사전#" .. entry .. "|" .. text .. "]]"
end


local function track(page)
	if type(page) == "table" then
		for i, pg in ipairs(page) do
			page[i] = "affix/" .. pg
		end
	else
		page = "affix/" .. page
	end
	require("Module:debug/track")(page)
end


local function ine(val)
	return val ~= "" and val or nil
end


-----------------------------------------------------------------------------------------
--                                      복합어 유형                                 --
-----------------------------------------------------------------------------------------

-- 복합어 유형 항목 생성 (용어사전 링크 사용)
local function make_compound_type(typ, alttext)
	return {
		text = glossary_link(typ, alttext) .. " 복합어",
		cat = typ .. " 복합어",
	}
end


-- 복합어 유형 항목 생성 (단순 링크 사용)
-- 추후 용어집에 항목이 생성되면 glossary_link로 대체해야 함
local function make_non_glossary_compound_type(typ, alttext)
	local link = alttext and "[[" .. typ .. "|" .. alttext .. "]]" or "[[" .. typ .. "]]"
	return {
		text = link .. " 복합어",
		cat = typ .. " 복합어",
	}
end

-- 원시 복합어 유형 항목 생성
local function make_raw_compound_type(typ, alttext)
	return {
		text = glossary_link(typ, alttext),
		cat = typ,
	}
end

-- 차용 유형 항목 생성
local function make_borrowing_type(typ, alttext)
	return {
		text = glossary_link(typ, alttext),
		borrowing_type = typ,
	}
end

-- 각종 어원 유형 및 약어 정의
export.etymology_types = {
	["adapted borrowing"] = make_borrowing_type("adapted borrowing", "순화 차용"),
	["adap"] = "adapted borrowing",
	["abor"] = "adapted borrowing",
	["alliterative"] = make_non_glossary_compound_type("alliterative", "두운"),
	["allit"] = "alliterative",
	["antonymous"] = make_non_glossary_compound_type("antonymous", "반의"),
	["ant"] = "antonymous",
	["bahuvrihi"] = make_compound_type("bahuvrihi", "bahuvrīhi", "바후브리히"),
	["bahu"] = "bahuvrihi",
	["bv"] = "bahuvrihi",
	["coordinative"] = make_compound_type("coordinative", "대등"),
	["coord"] = "coordinative",
	["descriptive"] = make_compound_type("descriptive", "기술"),
	["desc"] = "descriptive",
	["determinative"] = make_compound_type("determinative", "한정"),
	["det"] = "determinative",
	["dvandva"] = make_compound_type("dvandva", "드반드바"),
	["dva"] = "dvandva",
	["dvigu"] = make_compound_type("dvigu", "드비구"),
	["dvi"] = "dvigu",
	["endocentric"] = make_compound_type("endocentric", "내심"),
	["endo"] = "endocentric",
	["exocentric"] = make_compound_type("exocentric", "외심"),
	["exo"] = "exocentric",
	["izafet I"] = make_compound_type("izafet I", "이자페트 I"),
	["iz1"] = "izafet I",
	["izafet II"] = make_compound_type("izafet II", "이자페트 II"),
	["iz2"] = "izafet II",
	["izafet III"] = make_compound_type("izafet III", "이자페트 III"),
	["iz3"] = "izafet III",
	["karmadharaya"] = make_compound_type("karmadharaya", "karmadhāraya", "카르마다라야"),
	["karma"] = "karmadharaya",
	["kd"] = "karmadharaya",
	["kenning"] = make_raw_compound_type("kenning", "케닝"),
	["ken"] = "kenning",
	["rhyming"] = make_non_glossary_compound_type("rhyming", "압운"),
	["rhy"] = "rhyming",
	["synonymous"] = make_non_glossary_compound_type("synonymous", "동의"),
	["syn"] = "synonymous",
	["tatpurusa"] = make_compound_type("tatpurusa", "tatpuruṣa", "타트푸루샤"),
	["tat"] = "tatpurusa",
	["tp"] = "tatpurusa",
}

-- 어원 유형 처리
local function process_etymology_type(typ, nocap, notext, has_parts)
	local text_sections = {}
	local categories = {}
	local borrowing_type

	if typ then
		local typdata = export.etymology_types[typ]
		if type(typdata) == "string" then
			typdata = export.etymology_types[typdata]
		end
		if not typdata then
			error("내부 오류: 인식할 수 없는 유형 '" .. typ .. "'")
		end
		local text = typdata.text
		if not nocap then
			text = ucfirst(text)
		end
		local cat = typdata.cat
		borrowing_type = typdata.borrowing_type
		local oftext = typdata.oftext or "의 "

		if not notext then
		    if has_parts then
		        table.insert(text_sections, text)
		        table.insert(text_sections, oftext)
		    else
		        table.insert(text_sections, text)
		    end
		end
		if cat then
			table.insert(categories, cat)
		end
	end

	return text_sections, categories, borrowing_type
end


-----------------------------------------------------------------------------------------
--                                     유틸리티 함수                                   --
-----------------------------------------------------------------------------------------

-- 배열에서 가장 큰 정수 인덱스까지 반복
local function ipairs_with_gaps(t)
	local indices = m_table.numKeys(t)
	local max_index = #indices > 0 and math.max(unpack(indices)) or 0
	local i = 0
	return function()
		while i < max_index do
			i = i + 1
			return i, t[i]
		end
	end
end

export.ipairs_with_gaps = ipairs_with_gaps


--[==[
형식화된 부분들(`parts_formatted`)을 합치고, 직역(`lit`), 분류(`categories`) 등을 추가하는 함수.
`nocat`이 주어지면 분류를 추가하지 않음. `force_cat`은 사용자 문서 등에서도 분류를 강제로 추가함.
]==]
function export.join_formatted_parts(data)
	local cattext
	local lang = data.data.lang
	local force_cat = data.data.force_cat or debug_force_cat
	if data.data.nocat then
		cattext = ""
	else
		for i, cat in ipairs(data.categories) do
			local cat_base_string
			local sort_key
			local sort_base
			
			if type(cat) == "table" then
				cat_base_string = cat.cat
				sort_key = cat.sort_key
				sort_base = cat.sort_base
			else
				cat_base_string = cat
				sort_key = data.data.sort_key
				sort_base = nil
			end
			
			local full_cat_string
			if cat_base_string:find(" 붙은 ", 1, true) then
			    full_cat_string = cat_base_string:gsub(" 붙은 ", " 붙은 " .. lang:getFullName() .. " ", 1)
			else
				full_cat_string = lang:getFullName() .. " " .. cat_base_string
			end
			
			data.categories[i] = require(utilities_module).format_categories(full_cat_string, lang, sort_key, sort_base, force_cat)
		end
		cattext = table.concat(data.categories)
	end
	local result = table.concat(data.parts_formatted, " +&lrm; ") .. (data.data.lit and ", 직역하면 " ..
		m_links.mark(data.data.lit, "gloss") or "")
	local q = data.data.q
	local qq = data.data.qq
	local l = data.data.l
	local ll = data.data.ll
	if q and q[1] or qq and qq[1] or l and l[1] or ll and ll[1] then
		result = require(pron_qualifier_module).format_qualifiers {
			lang = lang,
			text = result,
			q = q,
			qq = qq,
			l = l,
			ll = ll,
		}
	end

	return result .. cattext
end


--[==[
join_formatted_parts()를 호출하는 구식 함수. 호출부를 수정해야 함.
]==]
function export.concat_parts(lang, parts_formatted, categories, nocat, sort_key, lit, force_cat)
	return export.join_formatted_parts {
		data = {
			lang = lang,
			nocat = nocat,
			sort_key = sort_key,
			lit = lit,
			force_cat = force_cat,
		},
		parts_formatted = parts_formatted,
		categories = categories,
	}
end


-- 링크를 제거하고 lang:makeEntryName(term) 호출
local function make_entry_name_no_links(lang, term)
	return (lang:makeEntryName(m_links.remove_links(term)))
end

--[=[
매개변수로 넘어온 원시(raw) 부분을 링크 가능한 부분으로 변환.
전체 언어/스크립트를 기본값으로 사용하고, 용어에서 조각(#)을 파싱함.
]=]
local function canonicalize_part(part, lang, sc)
	if not part then
		return
	end
	-- Save the original (user-specified, part-specific) value of `lang`. If such a value is specified, we don't insert
	-- a '*fixed with' category, and we format the part using format_derived() in [[Module:etymology]] rather than
	-- full_link() in [[Module:links]].
	part.part_lang = part.lang
	part.lang = part.lang or lang
	part.sc = part.sc or sc
	local term = part.term
	if not term then
		return
	elseif not part.fragment then
		part.term, part.fragment = m_links.get_fragment(term)
	else
		part.term = m_links.get_fragment(term)
	end
end


--[==[
주어진 부분(`part`)의 정보를 바탕으로 링크된 부분을 생성.
`part.part_lang`이 지정되면 모듈:etymology의 `format_derived`를 호출하고,
아니면 모듈:links의 `full_link`를 호출.
]==]
function export.link_term(part, data)
	local result

	if part.part_lang then
		-- format_derived() processes per-part qualifiers, labels and references, but they end up on the wrong side
		-- of the source (at least on the left), so we need to move them up.
		local q = part.q
		local qq = part.qq
		local l = part.l
		local ll = part.ll
		local refs = part.refs
		part.q = nil
		part.qq = nil
		part.l = nil
		part.ll = nil
		part.refs = nil
		result = require(etymology_module).format_derived {
			lang = data.lang,
			terms = {part},
			sources = {part.lang},
			sort_key = data.sort_key,
			nocat = data.nocat,
			borrowing_type = data.borrowing_type,
			force_cat = data.force_cat or debug_force_cat,
			q = q,
			qq = qq,
			l = l,
			ll = ll,
			refs = refs,
		}
	else
		result = m_links.full_link(part, "term", nil, "show qualifiers")
	end

	return result
end

-- 스크립트 코드를 표준화 (예: fa-Arab -> Arab)
local function canonicalize_script_code(scode)
	return (scode:gsub("^.*%-", ""))
end


-----------------------------------------------------------------------------------------
--                                  접사 처리 함수                                     --
-----------------------------------------------------------------------------------------

-- 주어진 접사와 언어에 맞는 스크립트를 파악하고, 그에 맞는 하이픈 값들을 반환.
local function detect_script_and_hyphens(text, lang, sc)
	local scode
	if sc then
		scode = sc:getCode()
	else
		local possible_script_codes = lang:getScriptCodes()
		local num_possible_script_codes = m_table.length(possible_script_codes)
		if num_possible_script_codes == 0 then
			error("심각한 오류 발생! " .. lang:getCanonicalName() .. " 언어에 스크립트 코드가 없습니다.")
		end
		if num_possible_script_codes == 1 then
			scode = possible_script_codes[1]
		else
			local may_have_nondefault_hyphen = false
			for _, script_code in ipairs(possible_script_codes) do
				script_code = canonicalize_script_code(script_code)
				if template_hyphens[script_code] or display_hyphens[script_code] then
					may_have_nondefault_hyphen = true
					break
				end
			end
			if not may_have_nondefault_hyphen then
				scode = "Latn"
			else
				scode = lang:findBestScript(text):getCode()
			end
		end
	end
	scode = canonicalize_script_code(scode)
	local template_hyphen = template_hyphens[scode] or "-"
	local lookup_hyphen = lookup_hyphens[scode] or "-"
	local display_hyphen = display_hyphens[scode] or default_display_hyphen
	return scode, template_hyphen, display_hyphen, lookup_hyphen
end


--[=[
주어진 틀 접사(`term`)와 접사 유형(`affix_type`)에 따라, 관련된 틀 하이픈을
새로운 하이픈(`new_hyphen`)으로 변경하거나, 없는 경우 추가함.
]=]
local function reconstruct_term_per_hyphens(term, affix_type, scode, thyph_re, new_hyphen)
	local function get_hyphen(hyph)
		if type(new_hyphen) == "string" then
			return new_hyphen
		end
		return new_hyphen(scode, hyph)
	end

	if affix_type == "non-affix" then
		return term
	elseif affix_type == "circumfix" then
		local before, before_hyphen, after_hyphen, after = rmatch(term, "^(.*)" .. thyph_re .. " " .. thyph_re
			.. "(.*)$")
		if not before or ulen(term) <= 3 then
			-- Unlike with other types of affixes, don't try to add hyphens in the middle of the term to convert it to
			-- a circumfix. Also, if the term is just hyphen + space + hyphen, return it.
			return term
		end
		return before .. get_hyphen(before_hyphen) .. " " .. get_hyphen(after_hyphen) .. after
	elseif affix_type == "infix" or affix_type == "interfix" then
		local before_hyphen, middle, after_hyphen = rmatch(term, "^" .. thyph_re .. "(.*)" .. thyph_re .. "$")
		if before_hyphen and ulen(term) <= 1 then
			-- If the term is just a hyphen, return it.
			return term
		end
		return get_hyphen(before_hyphen) .. (middle or term) .. get_hyphen(after_hyphen)
	elseif affix_type == "prefix" then
		local middle, after_hyphen = rmatch(term, "^(.*)" .. thyph_re .. "$")
		if middle and ulen(term) <= 1 then
			-- If the term is just a hyphen, return it.
			return term
		end
		return (middle or term) .. get_hyphen(after_hyphen)
	elseif affix_type == "suffix" then
		local before_hyphen, middle = rmatch(term, "^" .. thyph_re .. "(.*)$")
		if before_hyphen and ulen(term) <= 1 then
			-- If the term is just a hyphen, return it.
			return term
		end
		return get_hyphen(before_hyphen) .. (middle or term)
	else
		error(("내부 오류: 인식할 수 없는 접사 유형 '%s'"):format(affix_type))
	end
end


--[=[
주어진 접사 변이형을 표준형으로 매핑하는 함수.
매핑 테이블은 언어별, ID별로 지정될 수 있음.
]=]
local function lookup_affix_mapping(affix, affix_type, lang, scode, thyph_re, lookup_hyph, affix_id)
	local function do_lookup(affix)
		-- Ensure that the affix uses lookup hyphens regardless of whether it used a different type of hyphens before
		-- or no hyphens.
		local lookup_affix = reconstruct_term_per_hyphens(affix, affix_type, scode, thyph_re, lookup_hyph)
		local function do_lookup_for_langcode(langcode)
			if export.langs_with_lang_specific_data[langcode] then
				local langdata = mw.loadData(export.affix_lang_data_module_prefix .. langcode)
				if langdata.affix_mappings then
					local mapping = langdata.affix_mappings[lookup_affix]
					if mapping then
						if type(mapping) == "table" then
							mapping = mapping[affix_id or false]
							if mapping then
								return mapping
							end
						else
							return mapping
						end
					end
				end
			end
		end

		-- If `lang` is an etymology-only language, look for a mapping both for it and its full parent.
		local langcode = lang:getCode()
		local mapping = do_lookup_for_langcode(langcode)
		if mapping then
			return mapping
		end
		local full_langcode = lang:getFullCode()
		if full_langcode ~= langcode then
			mapping = do_lookup_for_langcode(full_langcode)
			if mapping then
				return mapping
			end
		end
		return nil
	end

	if affix:find("%[%[") then
		return nil
	end

	-- Double parens because makeEntryName() returns multiple values. Yuck.
	return do_lookup(affix) or do_lookup((lang:makeEntryName(affix))) or nil
end


--[==[
주어진 틀 용어(`term`)에 대해 접사 유형, 링크용어, 표시용어, 조회용어를 반환하는 핵심 분석 함수.
]==]
local function parse_term_for_affixes(term, lang, sc, affix_type, do_affix_mapping, return_lookup_affix, affix_id)
	if not term then
		return "non-affix", nil, nil, nil
	end

	if term == "^" then
		-- Indicates a null term to emulate the behavior of {{suffix|foo||bar}}.
		term = ""
		return "non-affix", term, term, term
	end
		
	if term:find("^%^") then
		-- HACK! ^ at the beginning of Korean languages has a special meaning, triggering capitalization of the
		-- transliteration. Don't interpret it as "force non-affix" for those languages.
		local langcode = lang:getCode()
		if langcode ~= "ko" and langcode ~= "okm" and langcode ~= "jje" then
			-- Formerly we allowed ^ to force non-affix type; this is now handled using an inline modifier
			-- <naf>, <root>, etc. Throw an error for the moment when the old way is encountered.
			error("^ 기호를 사용하여 비접사 상태를 강제하는 것은 더 이상 지원되지 않습니다. 요소 뒤에 <naf> 또는 <root>와 같은 인라인 수정자를 사용하세요.")
		end
	end

	-- Remove an asterisk if the morpheme is reconstructed and add it back at the end.
	local reconstructed = ""
	if term:find("^%*") then
		reconstructed = "*"
		term = term:gsub("^%*", "")
	end

	local scode, thyph, dhyph, lhyph = detect_script_and_hyphens(term, lang, sc)
	thyph = "([" .. thyph .. "])"

	if not affix_type then
		if rfind(term, thyph .. " " .. thyph) then
			affix_type = "circumfix"
		else
			local has_beginning_hyphen = rfind(term, "^" .. thyph)
			local has_ending_hyphen = rfind(term, thyph .. "$")
			if has_beginning_hyphen and has_ending_hyphen then
				affix_type = "interfix"
			elseif has_ending_hyphen then
				affix_type = "prefix"
			elseif has_beginning_hyphen then
				affix_type = "suffix"
			else
				affix_type = "non-affix"
			end
		end
	end

	local link_term, display_term, lookup_term
	if affix_type == "non-affix" then
		link_term = term
		display_term = term
		lookup_term = term
	else
		display_term = reconstruct_term_per_hyphens(term, affix_type, scode, thyph, dhyph)
		if do_affix_mapping then
			link_term = lookup_affix_mapping(term, affix_type, lang, scode, thyph, lhyph, affix_id)
			-- The return value of lookup_affix_mapping() may be an affix mapping with lookup hyphens if a mapping
			-- was found, otherwise nil if a mapping was not found. We need to convert to display hyphens in
			-- either case, but in the latter case we can reuse the display term, which has already been converted.
			if link_term then
				link_term = reconstruct_term_per_hyphens(link_term, affix_type, scode, thyph, dhyph)
			else
				link_term = display_term
			end
		else
			link_term = display_term
		end
		if return_lookup_affix then
			lookup_term = reconstruct_term_per_hyphens(term, affix_type, scode, thyph, lhyph)
		else
			lookup_term = display_term
		end
	end

	link_term = reconstructed .. link_term
	display_term = reconstructed .. display_term
	lookup_term = reconstructed .. lookup_term

	return affix_type, link_term, display_term, lookup_term
end


--[==[
Add a hyphen to a term in the appropriate place, based on the specified affix type, stripping off any existing hyphens
in that place. For example, if `affix_type` == {"prefix"}, we'll add a hyphen onto the end if it's not already there (or
is of the wrong type). Three values are returned: the link term, display term and lookup term. This function is a thin
wrapper around `parse_term_for_affixes`; see the comments above that function for more information. Note that this
function is exposed externally because it is called by [[Module:category tree/affixes and compounds]]; see the comment
in `parse_term_for_affixes` for more information.
]==]
function export.make_affix(term, lang, sc, affix_type, do_affix_mapping, return_lookup_affix, affix_id)
	if not (affix_type == "prefix" or affix_type == "suffix" or affix_type == "circumfix" or affix_type == "infix" or
		affix_type == "interfix" or affix_type == "non-affix") then
		error("내부 오류: 유효하지 않은 접사 유형 " .. (affix_type or "(없음)"))
	end
	local _, link_term, display_term, lookup_term = parse_term_for_affixes(term, lang, sc, affix_type,
		do_affix_mapping, return_lookup_affix, affix_id)
	return link_term, display_term, lookup_term
end

-----------------------------------------------------------------------------------------
--                                     메인 진입점                                     --
-----------------------------------------------------------------------------------------

--[==[
==[{{affix}} 및 {{surface analysis}} 구현.
`data` 테이블은 표시에 필요한 모든 정보를 담고 있음.
* `.lang` (필수): 전체 언어 객체.
* `.parts` (필수): 표시할 접사 부분들의 목록.
* `.pos`: 전체 품사 (기본값: "낱말"). 분류에 사용됨.
경고: 이 함수는 `data`와 `.parts` 안의 개별 구조를 직접 수정함.
]==]
function export.show_affix(data)
	data.pos = data.pos or default_pos

	local text_sections, categories, borrowing_type =
		process_etymology_type(data.type, data.surface_analysis or data.nocap, data.notext, #data.parts > 0)
	data.borrowing_type = borrowing_type

	-- Process each part
	local parts_formatted = {}
	local whole_words = 0
	local is_affix_or_compound = false

	-- 모든 부분을 먼저 표준화하고 링크를 생성
	for i, part in ipairs_with_gaps(data.parts) do
		part = part or {}
		data.parts[i] = part
		canonicalize_part(part, data.lang, data.sc)

		-- Determine affix type and get link and display terms (see text at top of file). Store them in the part
		-- (in fields that won't clash with fields used by full_link() in [[Module:links]] or link_term()), so they
		-- can be used in the loop below when categorizing.
		part.affix_type, part.affix_link_term, part.affix_display_term = parse_term_for_affixes(part.term,
			part.lang, part.sc, part.type, not part.alt, nil, part.id)

		-- If link_term is an empty string, either a bare ^ was specified or an empty term was used along with inline
		-- modifiers. The intention in either case is not to link the term.
		part.term = ine(part.affix_link_term)
		-- If part.alt would be the same as part.term, make it nil, so that it isn't erroneously tracked as being
		-- redundant alt text.
		part.alt = part.alt or (part.affix_display_term ~= part.affix_link_term and part.affix_display_term) or nil

		-- Make a link for the part.
		table.insert(parts_formatted, export.link_term(part, data))
	end

	-- 그 다음 분류 작업을 수행
	for i, part in ipairs_with_gaps(data.parts) do
		local affix_type = part.affix_type
		if affix_type ~= "non-affix" then
			is_affix_or_compound = true

			-- Make a sort key. For the first part, use the second part as the sort key; the intention is that if the
			-- term has a prefix, sorting by the prefix won't be very useful so we sort by what follows, which is
			-- presumably the root.
			local part_sort_base = nil
			local part_sort = part.sort or data.sort_key

			if i == 1 and data.parts[2] and data.parts[2].term then
				local part2 = data.parts[2]
				-- If the second-part link term is empty, the user requested an unlinked term; avoid a wikitext error
				-- by using the alt value if available.
				part_sort_base = ine(part2.affix_link_term) or ine(part2.alt)
				if part_sort_base then
					part_sort_base = make_entry_name_no_links(part2.lang, part_sort_base)
				end
			end

			if part.pos and rfind(part.pos, "patronym") then
				table.insert(categories, {cat = "인명", sort_key = part_sort, sort_base = part_sort_base})
			end

			if data.pos ~= "낱말" and part.pos and rfind(part.pos, "diminutive") then
				table.insert(categories, {cat = "지소 " .. data.pos, sort_key = part_sort,
					sort_base = part_sort_base})
			end

			-- Don't add a '*fixed with' category if the link term is empty or is in a different language.
			if ine(part.affix_link_term) and not part.part_lang then
				local affix_type_ko
				if affix_type == "prefix" then affix_type_ko = "접두사"
				elseif affix_type == "suffix" then affix_type_ko = "접미사"
				elseif affix_type == "infix" then affix_type_ko = "접요사"
				elseif affix_type == "circumfix" then affix_type_ko = "양접사"
				elseif affix_type == "interfix" then affix_type_ko = "연결사"
				end
				
				if affix_type_ko then
					local affix_name = make_entry_name_no_links(part.lang, part.affix_link_term) .. (part.id and " (" .. part.id .. ")" or "")
					table.insert(categories, {cat = affix_type_ko .. " " .. m_ko_utils.allomorphy(affix_name, "sbj") .. " 붙은 " .. data.pos,
						sort_key = part_sort, sort_base = part_sort_base})
				end
			end
		else
			whole_words = whole_words + 1

			if whole_words == 2 then
				is_affix_or_compound = true
				if data.pos == default_pos then
				    table.insert(categories, "복합어")
				else
				    table.insert(categories, "복합 " .. data.pos)
				end
			end
		end
	end

	-- Make sure there was either an affix or a compound (two or more non-affix terms).
	if not is_affix_or_compound then
		error("매개변수에 접사가 포함되지 않았거나, 복합어가 아닙니다. 적어도 하나의 접사를 제공해주세요.")
	end

	if data.surface_analysis then
		local text = glossary_link("표면 분석") .. "에 따르면, "
		if not data.nocap then
			text = ucfirst(text)
		end

		table.insert(text_sections, 1, text)
	end

	table.insert(text_sections, export.join_formatted_parts { data = data, parts_formatted = parts_formatted,
		categories = categories })
	return table.concat(text_sections)
end


function export.show_surface_analysis(data)
	data.surface_analysis = true
	return export.show_affix(data)
end


--[==[
{{compound}} 구현.
경고: 이 함수는 `data`와 `.parts` 안의 개별 구조를 직접 수정함.
]==]
function export.show_compound(data)
	data.pos = data.pos or default_pos

	local text_sections, categories, borrowing_type =
		process_etymology_type(data.type, data.nocap, data.notext, #data.parts > 0)
	data.borrowing_type = borrowing_type

	local parts_formatted = {}
	if data.pos == default_pos then
	    table.insert(categories, "복합어")
	else
	    table.insert(categories, "복합 " .. data.pos)
	end

	-- Make links out of all the parts
	local whole_words = 0
	for i, part in ipairs(data.parts) do
		canonicalize_part(part, data.lang, data.sc)
		local affix_type, link_term, display_term = parse_term_for_affixes(part.term, part.lang, part.sc,
			part.type, not part.alt, nil, part.id)
			
		if affix_type == "interfix" or (part.type and part.type ~= "non-affix") then
			if link_term and link_term ~= "" and not part.part_lang then
				table.insert(categories, {cat = data.pos .. " " .. affix_type .. "ed with " ..
					make_entry_name_no_links(part.lang, link_term), sort_key = part.sort or data.sort_key})
			end
			part.term = link_term ~= "" and link_term or nil
			part.alt = part.alt or (display_term ~= link_term and display_term) or nil
		else
			if affix_type ~= "non-affix" then
				local langcode = data.lang:getCode()
				track { affix_type, affix_type .. "/lang/" .. langcode }
				local full_langcode = data.lang:getFullCode()
				if langcode ~= full_langcode then
					track(affix_type .. "/lang/" .. full_langcode)
				end
			else
				whole_words = whole_words + 1
			end
		end
		table.insert(parts_formatted, export.link_term(part, data))
	end

	if whole_words == 1 then
		track("one whole word")
	elseif whole_words == 0 then
		track("looks like confix")
	end

	table.insert(text_sections, export.join_formatted_parts { data = data, parts_formatted = parts_formatted,
		categories = categories })
	return table.concat(text_sections)
end


--[==[
{{blend}}, {{univerbation}} 등 구현.
]==]
function export.show_compound_like(data)
	local parts_formatted = {}
	local categories = {}

	if data.cat then
		table.insert(categories, data.cat)
	end

	-- Make links out of all the parts
	for i, part in ipairs(data.parts) do
		canonicalize_part(part, data.lang, data.sc)
		table.insert(parts_formatted, export.link_term(part, data))
	end

	local text_sections = {}
	
	table.insert(text_sections, export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories })
	
	if #data.parts > 0 and data.oftext then
		table.insert(text_sections, data.oftext)
	end
	if data.text then
		table.insert(text_sections, " ")
		table.insert(text_sections, data.text)
	end
	
	return table.concat(text_sections)
end


--[==[
주어진 부분을 특정 유형의 접사로 만들고, 관련 매핑을 적용.
구식 틀들({{prefix}}, {{suffix}} 등)에서 사용됨.
경고: 이 함수는 `part`를 직접 수정함.
]==]
local function make_part_into_affix(part, lang, sc, affix_type)
	canonicalize_part(part, lang, sc)
	local link_term, display_term = export.make_affix(part.term, part.lang, part.sc, affix_type, not part.alt, nil, part.id)
	part.term = link_term
	-- When we don't specify `do_affix_mapping` to make_affix(), link and display terms (first and second retvals of
	-- make_affix()) are the same.
	-- If part.alt would be the same as part.term, make it nil, so that it isn't erroneously tracked as being
	-- redundant alt text.
	part.alt = part.alt and export.make_affix(part.alt, part.lang, part.sc, affix_type) or (display_term ~= link_term and display_term) or nil
	local Latn = require(scripts_module).getByCode("Latn")
	part.tr = export.make_affix(part.tr, part.lang, Latn, affix_type)
	part.ts = export.make_affix(part.ts, part.lang, Latn, affix_type)
end

-- 잘못된 접사 유형 사용을 추적하는 도우미 함수
local function track_wrong_affix_type(template, part, expected_affix_type)
	if part and not part.type then
		local affix_type = parse_term_for_affixes(part.term, part.lang, part.sc)
		if affix_type ~= expected_affix_type then
			local part_name = expected_affix_type or "base"
			local langcode = part.lang:getCode()
			local full_langcode = part.lang:getFullCode()
			require("Module:debug/track") {
				template,
				template .. "/" .. part_name,
				template .. "/" .. part_name .. "/" .. (affix_type or "none"),
				template .. "/" .. part_name .. "/" .. (affix_type or "none") .. "/lang/" .. langcode
			}
			-- If `part.lang` is an etymology-only language, track both using its code and its full parent's code.
			if full_langcode ~= langcode then
				require("Module:debug/track")(
					template .. "/" .. part_name .. "/" .. (affix_type or "none") .. "/lang/" .. full_langcode
				)
			end
		end
	end
end

-- 접사 분류를 추가하는 도우미 함수
local function insert_affix_category(categories, pos, affix_type, part, sort_key, sort_base)
	if part.term and not part.part_lang then
		local affix_name = make_entry_name_no_links(part.lang, part.term) .. (part.id and " (" .. part.id .. ")" or "")
		
		local affix_type_ko
		if affix_type == "prefix" then affix_type_ko = "접두사"
		elseif affix_type == "suffix" then affix_type_ko = "접미사"
		elseif affix_type == "infix" then affix_type_ko = "접요사"
		elseif affix_type == "circumfix" then affix_type_ko = "양접사"
		elseif affix_type == "interfix" then affix_type_ko = "연결사"
		end

		if affix_type_ko then
			local cat = affix_type_ko .. " " .. m_ko_utils.allomorphy(affix_name, "sbj") .. " 붙은 " .. pos
			if sort_key or sort_base then
				table.insert(categories, {cat = cat, sort_key = sort_key, sort_base = sort_base})
			else
				table.insert(categories, cat)
			end
		end
	end
end


--[==[
{{circumfix}} 구현.
경고: 이 함수는 `data`, `.prefix`, `.base`, `.suffix`를 직접 수정함.
]==]
function export.show_circumfix(data)
	data.pos = data.pos or default_pos

	canonicalize_part(data.base, data.lang, data.sc)
	-- Hyphenate the affixes and apply any affix mappings.
	make_part_into_affix(data.prefix, data.lang, data.sc, "prefix")
	make_part_into_affix(data.suffix, data.lang, data.sc, "suffix")

	track_wrong_affix_type("circumfix", data.prefix, "prefix")
	track_wrong_affix_type("circumfix", data.base, nil)
	track_wrong_affix_type("circumfix", data.suffix, "suffix")

	-- Create circumfix term.
	local circumfix = nil

	if data.prefix.term and data.suffix.term then
		circumfix = data.prefix.term .. " " .. data.suffix.term
		data.prefix.alt = data.prefix.alt or data.prefix.term
		data.suffix.alt = data.suffix.alt or data.suffix.term
		data.prefix.term = circumfix
		data.suffix.term = circumfix
	end

	-- Make links out of all the parts.
	local parts_formatted = {}
	local categories = {}
	local sort_base
	if data.base.term then
		sort_base = make_entry_name_no_links(data.base.lang, data.base.term)
	end

	table.insert(parts_formatted, export.link_term(data.prefix, data))
	table.insert(parts_formatted, export.link_term(data.base, data))
	table.insert(parts_formatted, export.link_term(data.suffix, data))

	-- Insert the categories, but don't add a '*fixed with' category if the link term is in a different language.
	if not data.prefix.part_lang then
		local affix_name = make_entry_name_no_links(data.prefix.lang, circumfix)
		table.insert(categories, {cat=affix_name .. " 양접사가 붙은 " .. data.pos, sort_key=data.sort_key, sort_base=sort_base})
	end

	return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end


--[==[
{{confix}} 구현.
]==]
function export.show_confix(data)
	data.pos = data.pos or default_pos

	canonicalize_part(data.base, data.lang, data.sc)
	-- Hyphenate the affixes and apply any affix mappings.
	make_part_into_affix(data.prefix, data.lang, data.sc, "prefix")
	make_part_into_affix(data.suffix, data.lang, data.sc, "suffix")

	track_wrong_affix_type("confix", data.prefix, "prefix")
	track_wrong_affix_type("confix", data.base, nil)
	track_wrong_affix_type("confix", data.suffix, "suffix")

	-- Make links out of all the parts.
	local parts_formatted = {}
	local prefix_sort_base
	if data.base and data.base.term then
		prefix_sort_base = make_entry_name_no_links(data.base.lang, data.base.term)
	elseif data.suffix.term then
		prefix_sort_base = make_entry_name_no_links(data.suffix.lang, data.suffix.term)
	end

	-- Insert the categories and parts.
	local categories = {}

	table.insert(parts_formatted, export.link_term(data.prefix, data))
	insert_affix_category(categories, data.pos, "prefix", data.prefix, data.sort_key, prefix_sort_base)

	if data.base then
		table.insert(parts_formatted, export.link_term(data.base, data))
	end

	table.insert(parts_formatted, export.link_term(data.suffix, data))
	-- FIXME, should we be specifying a sort base here?
	insert_affix_category(categories, data.pos, "suffix", data.suffix)

	return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end


--[==[
{{infix}} 구현.
]==]
function export.show_infix(data)
	data.pos = data.pos or default_pos

	canonicalize_part(data.base, data.lang, data.sc)
	-- Hyphenate the affixes and apply any affix mappings.
	make_part_into_affix(data.infix, data.lang, data.sc, "infix")

	track_wrong_affix_type("infix", data.base, nil)
	track_wrong_affix_type("infix", data.infix, "infix")

	-- Make links out of all the parts.
	local parts_formatted = {}
	local categories = {}

	table.insert(parts_formatted, export.link_term(data.base, data))
	table.insert(parts_formatted, export.link_term(data.infix, data))

	-- Insert the categories.
	-- FIXME, should we be specifying a sort base here?
	insert_affix_category(categories, data.pos, "infix", data.infix)

	return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end


--[==[
{{prefix}} 구현.

'''WARNING''': This destructively modifies both `data` and the structures within `.prefixes`, as well as `.base`.
]==]
function export.show_prefix(data)
	data.pos = data.pos or default_pos

	canonicalize_part(data.base, data.lang, data.sc)
	-- Hyphenate the affixes and apply any affix mappings.
	for i, prefix in ipairs(data.prefixes) do
		make_part_into_affix(prefix, data.lang, data.sc, "prefix")
	end

	for i, prefix in ipairs(data.prefixes) do
		track_wrong_affix_type("prefix", prefix, "prefix")
	end

	track_wrong_affix_type("prefix", data.base, nil)

	-- Make links out of all the parts.
	local parts_formatted = {}
	local first_sort_base = nil
	local categories = {}

	if data.prefixes[2] then
		first_sort_base = ine(data.prefixes[2].term) or ine(data.prefixes[2].alt)
		if first_sort_base then
			first_sort_base = make_entry_name_no_links(data.prefixes[2].lang, first_sort_base)
		end
	elseif data.base then
		first_sort_base = ine(data.base.term) or ine(data.base.alt)
		if first_sort_base then
			first_sort_base = make_entry_name_no_links(data.base.lang, first_sort_base)
		end
	end

	for i, prefix in ipairs(data.prefixes) do
		table.insert(parts_formatted, export.link_term(prefix, data))
		insert_affix_category(categories, data.pos, "prefix", prefix, data.sort_key, i == 1 and first_sort_base or nil)
	end

	if data.base then
		table.insert(parts_formatted, export.link_term(data.base, data))
	else
		table.insert(parts_formatted, "")
	end

	return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end


--[==[
{{suffix}} 구현.

'''WARNING''': This destructively modifies both `data` and the structures within `.suffixes`, as well as `.base`.
]==]
function export.show_suffix(data)
	local categories = {}

	data.pos = data.pos or default_pos

	canonicalize_part(data.base, data.lang, data.sc)
	-- Hyphenate the affixes and apply any affix mappings.
	for i, suffix in ipairs(data.suffixes) do
		make_part_into_affix(suffix, data.lang, data.sc, "suffix")
	end

	track_wrong_affix_type("suffix", data.base, nil)
	for i, suffix in ipairs(data.suffixes) do
		track_wrong_affix_type("suffix", suffix, "suffix")
	end

	-- Make links out of all the parts.
	local parts_formatted = {}

	if data.base then
		table.insert(parts_formatted, export.link_term(data.base, data))
	else
		table.insert(parts_formatted, "")
	end

	for i, suffix in ipairs(data.suffixes) do
		table.insert(parts_formatted, export.link_term(suffix, data))
	end

	-- Insert the categories.
	for i, suffix in ipairs(data.suffixes) do
		-- FIXME, should we be specifying a sort base here?
		insert_affix_category(categories, data.pos, "suffix", suffix)

		if suffix.pos and rfind(suffix.pos, "patronym") then
			table.insert(categories, "인명")
		end
	end

	return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end

return export