Module:family tree: Difference between revisions

From Linguifex
Jump to navigation Jump to search
No edit summary
No edit summary
 
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
--[=[
--[=[


Authors: [[User:kc_kennylau]], [[User:JohnC5]], [[User:Erutuon]], [[User:Suzukaze-c]]
Authors: [[User:kc_kennylau]], [[User:JohnC5]], [[User:Erutuon]], [[User:Suzukaze-c]], [[User:Theknightwho]], [[User:AryamanA]]


--]=]
--]=]


local p = {}
local export = {}


local regular_languages = require("Module:languages/alldata")
local regular_languages = require("Module:languages/code to canonical name")
local families = require("Module:families/data")
local etymology_languages = require("Module:etymology languages/code to canonical name")
local families = require("Module:families/code to canonical name")


-- Version of [[Module:etymology languages/data]] that chooses the language-
function export.find_subtree(t, code)
-- codiest code of several codes that have the same data. For instance,
-- it chooses "de-AT" over "Austrian German".
local etymology_languages = require("Module:family tree/etymology languages")
 
local Array = require("Module:array")
 
function p.find_subtree(t, code)
for _, val in ipairs(t) do
for _, val in ipairs(t) do
if val.name == code then -- "name" is really code
if val.name == code then -- "name" is really code
return { val }
return {val}
else
end
local result = p.find_subtree(val, code)
local result = export.find_subtree(val, code)
if result then
if result ~= nil then
return result
return result
end
end
end
end
end
Line 31: Line 24:


local family_icon = "F"
local family_icon = "F"
local etymology_language_icon = "E"
local variety_icon = "V"
local proto_language_icon = family_icon
local proto_language_icon = family_icon
local family_with_proto_language_icon = family_icon
local family_with_proto_language_icon = family_icon
local function format_node(code, is_protolanguage_or_has_protolanguage)
local function format_node(code, is_protolanguage_or_has_protolanguage, options)
local canonical_name, category_name, class, icon, tooltip
local canonical_name, category_name, class, icon, tooltip, lemma_count_text
if regular_languages[code] then
if regular_languages[code] then
canonical_name = regular_languages[code][1]  
canonical_name = regular_languages[code]
category_name = canonical_name .. ' language'
category_name = canonical_name:match(" [Ll]anguage$") and canonical_name or canonical_name .. " language"
class = "familytree-lang"
class = "familytree-lang"
style = ''
if is_protolanguage_or_has_protolanguage then
if is_protolanguage_or_has_protolanguage then
class = class .. ' familytree-protolang'
icon = proto_language_icon
icon = proto_language_icon
end
-- Add lemma count if the lemma_count option is set and category name exists
if options and options.lemma_count and category_name then
page_count = mw.site.stats.pagesInCategory(
canonical_name .. " lemmas",
"pages"
)
lemma_count_text = ' (' .. page_count .. ')'
end
end
elseif etymology_languages[code] then
elseif etymology_languages[code] then
canonical_name = etymology_languages[code].canonicalName
canonical_name = etymology_languages[code]
class = "familytree-etymlang"
class = "familytree-etymlang"
icon = etymology_language_icon
icon = variety_icon
tooltip = "Etymology language"
tooltip = "Variety"
elseif families[code] then
elseif families[code] then
canonical_name = families[code].canonicalName
canonical_name = families[code]
category_name = canonical_name .. " languages"
category_name = (canonical_name:match(" [Ll]anguages$") or canonical_name:match(" [Ll]ects$")) and canonical_name or
canonical_name .. " languages"
class = "familytree-family"
class = "familytree-family"
if is_protolanguage_or_has_protolanguage then
if is_protolanguage_or_has_protolanguage then
Line 66: Line 69:
.. '[[:Category:' .. (category_name or canonical_name) .. '|'
.. '[[:Category:' .. (category_name or canonical_name) .. '|'
.. canonical_name
.. canonical_name
.. ' <span class="familytree-code" style="color: green;">(' .. code .. ')</span>]]'
.. ' <span class="familytree-code">(' .. code .. ')</span>]]'
.. (icon and ' <span class="familytree-icon" style="background: '.. (class == "familytree-etymlang" and "#005ab3" or "green") .. ';">'
.. (icon and ' <span class="familytree-icon">' .. icon .. '</span>' or '')
.. icon .. '</span>' or '')
-- Include lemma count text if available
.. (lemma_count_text or '')
.. '</span>'
.. '</span>'
end
end
Line 119: Line 123:
end
end


local function make_node(code, is_protolanguage, protolanguage_of)
local function make_node(code, is_protolanguage, protolanguage_of, options)
return '</span> ' .. format_node(code,
return '</span> ' .. format_node(code,
is_protolanguage[code] or protolanguage_of[code] ~= nil)
is_protolanguage[code] or protolanguage_of[code] ~= nil, options)
end
end


Line 130: Line 134:
end
end


p.are_all_children_etymology_languages = require "Module:fun".memoize(function (nested_data)
export.are_all_children_etymology_languages = require("Module:memoize")(function (nested_data)
if not nested_data[1] then
if not nested_data[1] then
return nil
return nil
Line 137: Line 141:
for _, child in ipairs(nested_data) do
for _, child in ipairs(nested_data) do
if not etymology_languages[child.name]
if not etymology_languages[child.name]
or p.are_all_children_etymology_languages(child) == false then
or export.are_all_children_etymology_languages(child) == false then
return false
return false
end
end
Line 152: Line 156:
end
end


local no_break_space = mw.ustring.char(0xA0)
local no_break_space = "\194\160"
local level_separator = (no_break_space):rep(3)
local level_separator = (no_break_space):rep(3)
local expandtext, collapsetext = "[+]─", "[-]┬"
local expandtext, collapsetext = "[+]─", "[-]┬"
local function make_tree(data, is_protolanguage, protolanguage_of, options, prefix)
local function make_tree(data, is_protolanguage, protolanguage_of, options, prefix)
local result = Array()
local result = {}
local function ins(val)
table.insert(result, val)
end
-- This tag is closed in the node generated by make_node.
-- This tag is closed in the node generated by make_node.
Line 172: Line 179:
local code = val.name
local code = val.name
local language_or_family_node =
local language_or_family_node =
make_node(code, is_protolanguage, protolanguage_of)
make_node(code, is_protolanguage, protolanguage_of, options)
if not val[1] then
if not val[1] then
result:insert('<li>' .. prefix .. branch .. options.sterile_branch_text
ins('<li>' .. prefix .. branch .. options.sterile_branch_text
.. language_or_family_node .. '</li>')
.. language_or_family_node .. '</li>')
else
else
local customcollapsible_id = get_customcollapsible_id()
local customcollapsible_id = get_customcollapsible_id()
result:insert('<li>' .. prefix .. branch
ins('<li>' .. prefix .. branch
.. '<span class="familytree-toggle mw-customtoggle-'
.. '<span class="familytree-toggle mw-customtoggle-'
.. customcollapsible_id .. '">───┬</span>')
.. customcollapsible_id .. '">───┬</span>')
Line 194: Line 201:
val = val[1]
val = val[1]
top_node = make_node(code, is_protolanguage, protolanguage_of)
top_node = make_node(code, is_protolanguage, protolanguage_of, options)
if options.protolanguage_under_family then
if options.protolanguage_under_family then
top_node, language_or_family_node =
top_node, language_or_family_node =
Line 202: Line 209:
local all_children_are_etymology_languages =
local all_children_are_etymology_languages =
p.are_all_children_etymology_languages(val)
export.are_all_children_etymology_languages(val)
local collapsible_ul = '<ul class="mw-collapsible'
local collapsible_ul = '<ul class="mw-collapsible'
Line 213: Line 220:
if flag then
if flag then
result:insert(top_node
ins(top_node
.. collapsible_ul .. '<li>' .. prefix
.. collapsible_ul .. '<li>' .. prefix
.. (i == length and no_break_space or "│")
.. (i == length and no_break_space or "│")
Line 219: Line 226:
end
end
result:insert(language_or_family_node)
ins(language_or_family_node)
if not flag then
if not flag then
result:insert(collapsible_ul)
ins(collapsible_ul)
end
end
Line 228: Line 235:
-- and data-collapsetext attribute values to the custom toggle,
-- and data-collapsetext attribute values to the custom toggle,
-- so have to have a custom script do it.
-- so have to have a custom script do it.
result:insert(make_tree(val, is_protolanguage, protolanguage_of, options, next_level))
ins(make_tree(val, is_protolanguage, protolanguage_of, options, next_level))
result:insert('</ul></li>')
ins('</ul></li>')
end
end
end
end
return result:concat()
return table.concat(result)
end
end


Line 252: Line 259:
end
end


function p.show(frame)
function export.show(frame)
local args = frame.args
local args = frame.args
local descendants_of = args[1]
local descendants_of = args[1]
if descendants_of == "" then
descendants_of = nil
elseif not (regular_languages[descendants_of] or families[descendants_of]) then
error("The language code " .. descendants_of
.. " is not a valid non-etymology language or family.")
end
local to_boolean = require("Module:yesno")
local to_boolean = require("Module:yesno")
Line 270: Line 271:
-- Determines whether all etymology languages will be shown.
-- Determines whether all etymology languages will be shown.
local show_etymology_languages = to_boolean(args[3] or args.etym)
local show_etymology_languages = to_boolean(args[3] or args.etym)
-- Get the value for lemma_count argument
local lemma_count = to_boolean(args.lemma_count)
-- help! parameter name too long!
-- help! parameter name too long!
Line 284: Line 288:
end
end
return p.print_children(descendants_of, {
return export.print_children(descendants_of, {
hide_families_with_protolanguages = not show_all_families,
hide_families_with_protolanguages = not show_all_families,
hide_etymology_languages = not show_etymology_languages,
hide_etymology_languages = not show_etymology_languages,
Line 290: Line 294:
protolanguage_under_family = protolanguage_under_family,
protolanguage_under_family = protolanguage_under_family,
sterile_branch_length = sterile_branch_length,
sterile_branch_length = sterile_branch_length,
collapsed = require("Module:yesno")(args.collapsed)
collapsed = require("Module:yesno")(args.collapsed),
lemma_count = lemma_count
})
})
end
end


function p.print_children(descendants_of, options)
function export.print_children(descendants_of, options)
local data = require("Module:family tree/nested data")
local m_languages = require("Module:languages")
local m_table = require("Module:table")
local make_auto_subtabler = require("Module:auto-subtable")
descendants_of = m_languages.getByCode(descendants_of, nil, true, true)
local names = {}
local protolanguage_of = {}
local children = make_auto_subtabler{}
local descendants = descendants_of:getDescendantCodes()
table.insert(descendants, descendants_of:getCode())
if descendants_of:hasType("family") then
protolanguage_of[descendants_of:getCode()] = descendants_of:getProtoLanguageCode()
end
local memoized = {}
local get = function(code, func, ...)
local ret = memoized[code] or func(...)
if code then
memoized[code] = ret
end
return ret
end
for _, descendant_code in ipairs(descendants) do
-- Inner "repeat until true" loop allows break to work like continue, as it will always only run once.
repeat
local descendant = get(descendant_code, m_languages.getByCode, descendant_code, nil, true, true)
names[descendant_code] = descendant:getCanonicalName():gsub("Proto%-", "")
if descendant:hasType("language") then
local ancestors = m_table.shallowCopy(descendant:getAncestorCodes())
local parent_code = descendant:getParentCode()
if parent_code and descendant:hasType("etymology-only") then
local parent = get(parent_code, descendant.getParent, descendant)
if m_table.deepEquals(parent:getAncestorCodes(), ancestors) and
descendant:getFamilyCode() == parent:getFamilyCode() then
table.insert(children[parent:getCode()], descendant_code)
break
end
end
if #ancestors > 0 then
for _, ancestor in ipairs(ancestors) do
table.insert(children[ancestor], descendant_code)
end
break
end
else
local protolang = descendant:getProtoLanguageCode()
protolanguage_of[descendant_code] = protolang
if protolang and descendant:hasAncestor(protolang) then
table.insert(children[protolang], descendant_code)
break
end
end
local family_code = descendant:getFamilyCode()
if family_code then
local family = get(family_code, descendant.getFamily, descendant)
local protolang = get(family:getProtoLanguageCode(), family.getProtoLanguage, family)
if not protolanguage_of[family] then
protolanguage_of[family] = protolang and protolang:getCode()
end
if protolang and protolang:inFamily(family) and protolang:getCode() ~= descendant_code then
table.insert(children[protolang:getCode()], descendant_code)
else
table.insert(children[family:getCode()], descendant_code)
end
end
until true
end
-- No more auto subtabling needed.
children = children:un_auto_subtable()
-- Copy to new table, to filter out unwanted ancestors from descendants with multiple ancestors, where some are not descendants of the target language.
local parent_to_children_map = {}
for _, code in ipairs(descendants) do
parent_to_children_map[code] = children[code]
end
local function make_nested(data, children)
local make_nil = {}
for key, val in pairs(data) do
if type(key) == "number" then
if children[val] then
data[val] = make_nested(children[val], children)
table.insert(make_nil, key)
end
else
data[key] = make_nested(val, children)
end
end
if make_nil[2] then -- Make sure larger keys are removed first.
table.sort(make_nil, function (a, b) return a > b end)
end
for _, key in ipairs(make_nil) do
table.remove(data, key)
end
return data
end
local nested = make_nested(parent_to_children_map, parent_to_children_map)
local function deep_sort(current)
local result = {}
local is_table = {}
for key, val in pairs(current) do
if type(key) == "number" then
table.insert(result, val)
else
is_table[key] = true
table.insert(result, key)
end
end
table.sort(result, function(code1, code2)
return names[code1] < names[code2]
end)
for i = 1, #result do
if is_table[result[i]] then
local name = result[i]
result[i] = deep_sort(current[result[i]])
result[i].name = name
else
result[i] = { name = result[i] }
end
end
return result
end
nested = deep_sort(nested)
data = { nested = nested, protolanguage_of = protolanguage_of }
local nested_data, protolanguage_of = data.nested, data.protolanguage_of
local nested_data, protolanguage_of = data.nested, data.protolanguage_of
if descendants_of then
nested_data = export.find_subtree(nested_data, descendants_of:getCode())
nested_data = p.find_subtree(nested_data, descendants_of)
end
-- Return nil instead of a tree with only the root node.
-- Return nil instead of a tree with only the root node.
if options.must_have_descendants and (#nested_data == 0 or nested_data[1] and #nested_data[1] == 0) then
if options.must_have_descendants and (nested_data == nil or #nested_data == 0 or nested_data[1] and #nested_data[1] == 0) then
return nil
return nil
end
end
Line 319: Line 459:
end
end
local result = Array('<div class="familytree"><ul>')
local result = {'<div class="familytree"><ul>'}
local function ins(val)
table.insert(result, val)
end
local tree_options = {
local tree_options = {
Line 327: Line 470:
family_under_protolanguage = options.family_under_protolanguage,
family_under_protolanguage = options.family_under_protolanguage,
protolanguage_under_family = options.protolanguage_under_family,
protolanguage_under_family = options.protolanguage_under_family,
lemma_count = options.lemma_count,
}
}
Line 332: Line 476:
for i, subtable in ipairs(nested_data) do
for i, subtable in ipairs(nested_data) do
-- top language name
-- top language name
result:insert('<li>')
ins('<li>')
-- name me!
-- name me!
Line 349: Line 493:
end
end
result:insert(top_node)
ins(top_node)
-- top toggle
-- top toggle
local customcollapsible_id = get_customcollapsible_id()
local customcollapsible_id = get_customcollapsible_id()
result:insert('<span class="familytree-toptoggle mw-customtoggle-'
ins('<span class="familytree-toptoggle mw-customtoggle-'
.. customcollapsible_id .. '" style="display: none;">')
.. customcollapsible_id .. '" style="display: none;">')
result:insert(options.collapsed and expandtext or collapsetext)
ins(options.collapsed and expandtext or collapsetext)
result:insert('</span>')
ins('</span>')
if flag then
if flag then
result:insert('<li>')
ins('<li>')
result:insert(next_node)
ins(next_node)
end
end
-- tree
-- tree
result:insert('<ul class="mw-collapsible')
ins('<ul class="mw-collapsible')
if options.collapsed then
if options.collapsed then
result:insert(' mw-collapsed')
ins(' mw-collapsed')
end
end
result:insert('" id="mw-customcollapsible-' .. customcollapsible_id)
ins('" id="mw-customcollapsible-' .. customcollapsible_id)
result:insert('" data-expandtext="' .. expandtext)
ins('" data-expandtext="' .. expandtext)
result:insert('" data-collapsetext="' .. collapsetext .. '">')
ins('" data-collapsetext="' .. collapsetext .. '">')
result:insert(make_tree(subtable, is_protolanguage, protolanguage_of, tree_options, nil))
ins(make_tree(subtable, is_protolanguage, protolanguage_of, tree_options))
if flag then
if flag then
result:insert('</li>')
ins('</li>')
end
end
result:insert('</ul></li>')
ins('</ul></li>')
end
end
result:insert('</ul></div>')
ins('</ul></div>')
ins(require("Module:TemplateStyles")("Module:family tree/style.css"))
return result:concat()
end
 
function p.print_data(frame)
return require("Module:debug").highlight_dump(require("Module:family tree/nested data"))
end
 
-- Mainly for testing.
function p.list_descendants_of_frame(frame)
local parent = frame.args[1]
if not parent then
error("Provide parent code in parameter 1.")
end
return table.concat(p.list_descendants_of(parent), "\n")
end
 
function p.list_descendants_of(parent)
local subtree = p.find_subtree(require("Module:family tree/nested data").nested, parent)
local function gather_descendant_codes(subtree, descendants)
for _, lang in ipairs(subtree) do
table.insert(descendants, lang.name)
gather_descendant_codes(lang, descendants)
end
end
local codes = {}
return table.concat(result)
gather_descendant_codes(subtree[1], codes)
table.sort(codes)
return codes
end
end


return p
return export

Latest revision as of 19:41, 9 January 2025



--[=[

Authors: [[User:kc_kennylau]], [[User:JohnC5]], [[User:Erutuon]], [[User:Suzukaze-c]], [[User:Theknightwho]], [[User:AryamanA]]

--]=]

local export = {}

local regular_languages = require("Module:languages/code to canonical name")
local etymology_languages = require("Module:etymology languages/code to canonical name")
local families = require("Module:families/code to canonical name")

function export.find_subtree(t, code)
	for _, val in ipairs(t) do
		if val.name == code then -- "name" is really code
			return {val}
		end
		local result = export.find_subtree(val, code)
		if result ~= nil then
			return result
		end
	end
end

local family_icon = "F"
local variety_icon = "V"
local proto_language_icon = family_icon
local family_with_proto_language_icon = family_icon
local function format_node(code, is_protolanguage_or_has_protolanguage, options)
	local canonical_name, category_name, class, icon, tooltip, lemma_count_text
	if regular_languages[code] then
		canonical_name = regular_languages[code]
		category_name = canonical_name:match(" [Ll]anguage$") and canonical_name or canonical_name .. " language"
		class = "familytree-lang"
		if is_protolanguage_or_has_protolanguage then
			class = class .. ' familytree-protolang'
			icon = proto_language_icon
		end
		
		-- Add lemma count if the lemma_count option is set and category name exists
		if options and options.lemma_count and category_name then
			page_count = mw.site.stats.pagesInCategory(
				canonical_name .. " lemmas",
				"pages"
			)
			lemma_count_text = ' (' .. page_count .. ')'
		end
	elseif etymology_languages[code] then
		canonical_name = etymology_languages[code]
		class = "familytree-etymlang"
		icon = variety_icon
		tooltip = "Variety"
	elseif families[code] then
		canonical_name = families[code]
		category_name = (canonical_name:match(" [Ll]anguages$") or canonical_name:match(" [Ll]ects$")) and canonical_name or
			canonical_name .. " languages"
		class = "familytree-family"
		if is_protolanguage_or_has_protolanguage then
			class = class .. ' familytree-hasprotolang'
			icon = family_with_proto_language_icon
		else
			icon = family_icon
		end
		tooltip = "Language family"
	end
	
	return '<span class="' .. class .. '" '
		.. (tooltip and 'title="' .. tooltip .. '"' or '') .. '>'
		.. '[[:Category:' .. (category_name or canonical_name) .. '|'
		.. canonical_name
		.. ' <span class="familytree-code">(' .. code .. ')</span>]]'
		.. (icon and ' <span class="familytree-icon">' .. icon .. '</span>' or '')
		-- Include lemma count text if available
		.. (lemma_count_text or '')
		.. '</span>'
end

-- If neither options.show_all_families or options.show_etymology_languages is
-- falsy, then this function does nothing.
local function filter_nested_data(nested_data, options, protolanguage_of, is_protolanguage)
	if not nested_data then -- ???
		return nil
	else
		local name = nested_data.name
		local first_child = nested_data[1]
		
		-- This indicates that new_nested_data below should only be returned
		-- if it contains non-etymology languages.
		local check_for_non_etymology_children = false
		
		-- If `show_all_families` is false and this is a family and its only
		-- child is its proto-language, then replace the family with the
		-- proto-language.
		if options.hide_families_with_protolanguages and name and families[name]
		and first_child and not nested_data[2]
		and protolanguage_of[name] == first_child.name then
			is_protolanguage[first_child.name] = true
			return filter_nested_data(first_child, options, protolanguage_of, is_protolanguage)
		
		elseif options.hide_etymology_languages
		and etymology_languages[name] then
			if nested_data[1] then
				check_for_non_etymology_children = true
			else
				return nil
			end
		end
		
		local new_nested_data = { name = name }
		local i = 0
		for _, subtable in ipairs(nested_data) do
			subtable = filter_nested_data(subtable, options, protolanguage_of, is_protolanguage)
			if subtable then
				i = i + 1
				new_nested_data[i] = subtable
			end
		end
		
		if not check_for_non_etymology_children or new_nested_data[1] then
			return new_nested_data
		end
	end
end

local function make_node(code, is_protolanguage, protolanguage_of, options)
	return '</span> ' .. format_node(code,
		is_protolanguage[code] or protolanguage_of[code] ~= nil, options)
end

local function only_child_is_protolanguage(tree, options, protolanguage_of)
	return (options.family_under_protolanguage
		or options.protolanguage_under_family)
		and tree[1] and protolanguage_of[tree.name] == tree[1].name
end

export.are_all_children_etymology_languages = require("Module:memoize")(function (nested_data)
	if not nested_data[1] then
		return nil
	end
	
	for _, child in ipairs(nested_data) do
		if not etymology_languages[child.name]
		or export.are_all_children_etymology_languages(child) == false then
			return false
		end
	end
	
	return true
end)

local customcollapsible_number = 0
local customcollapsible_prefix = "familytree"
local function get_customcollapsible_id()
	customcollapsible_number = customcollapsible_number + 1
	return customcollapsible_prefix .. customcollapsible_number
end

local no_break_space = "\194\160"
local level_separator = (no_break_space):rep(3)
local expandtext, collapsetext = "[+]─", "[-]┬"
local function make_tree(data, is_protolanguage, protolanguage_of, options, prefix)
	local result = {}
	local function ins(val)
		table.insert(result, val)
	end
	
	-- This tag is closed in the node generated by make_node.
	prefix = prefix or '<span class="familytree-linedrawing">'
	
	local branch = "├"
	local next_level = prefix .. "│" .. level_separator
	local length = #data
	for i, val in ipairs(data) do
		if i == length then
			branch = "└"
			next_level = prefix .. level_separator .. no_break_space
		end
		
		local code = val.name
		local language_or_family_node =
			make_node(code, is_protolanguage, protolanguage_of, options)
		
		if not val[1] then
			ins('<li>' .. prefix .. branch .. options.sterile_branch_text
				.. language_or_family_node .. '</li>')
		else
			local customcollapsible_id = get_customcollapsible_id()
			
			ins('<li>' .. prefix .. branch
				.. '<span class="familytree-toggle mw-customtoggle-'
				.. customcollapsible_id .. '">───┬</span>')
			
			-- name me!
			local flag = (options.family_under_protolanguage
				or options.protolanguage_under_family)
				and only_child_is_protolanguage(val, options, protolanguage_of)
			
			local top_node
			if flag then
				code = val[1].name
				val = val[1]
				
				top_node = make_node(code, is_protolanguage, protolanguage_of, options)
				if options.protolanguage_under_family then
					top_node, language_or_family_node =
						language_or_family_node, top_node
				end
			end
				
			local all_children_are_etymology_languages =
				export.are_all_children_etymology_languages(val)
			
			local collapsible_ul = '<ul class="mw-collapsible'
				.. (all_children_are_etymology_languages
					and ' familytree-only-etym-children'
					or '') .. '" '
				.. 'id="mw-customcollapsible-' .. customcollapsible_id
				.. '" data-expandtext="' .. expandtext
				.. '" data-collapsetext="' .. collapsetext .. '">'
			
			if flag then
				ins(top_node
					.. collapsible_ul .. '<li>' .. prefix
					.. (i == length and no_break_space or "│")
					.. level_separator .. "│")
			end
			
			ins(language_or_family_node)
			
			if not flag then
				ins(collapsible_ul)
			end
			
			-- Can't get default collapsibility script to apply the data-expandtext
			-- and data-collapsetext attribute values to the custom toggle,
			-- so have to have a custom script do it.
			ins(make_tree(val, is_protolanguage, protolanguage_of, options, next_level))
			ins('</ul></li>')
		end
	end
	return table.concat(result)
end

local function get_number_parameter_in_range(args, arg, low, high)
	local val = args[arg]
	
	if val == "" or val == nil then
		val = nil
	else
		val = tonumber(val)
		if not (type(val) == "number"
		and 0 <= val and val <= 6) then
			error("Expected nothing or number between " .. low .. " and "
				.. high .. " in parameter |" .. arg .. "=.")
		end
	end
	
	return val
end

function export.show(frame)
	local args = frame.args
	
	local descendants_of = args[1]
	
	local to_boolean = require("Module:yesno")
	
	-- Determines whether families that have proto-languages will be shown.
	local show_all_families = to_boolean(args[2] or args.fam)
	
	-- Determines whether all etymology languages will be shown.
	local show_etymology_languages = to_boolean(args[3] or args.etym)

	-- Get the value for lemma_count argument
	local lemma_count = to_boolean(args.lemma_count)
	
	-- help! parameter name too long!
	local sterile_branch_length = get_number_parameter_in_range(args, "sterile_branch_length", 0, 6)
	
	-- Determines whether (if all families are shown) a family will be shown
	-- on a line directly under and at the same level as its proto-language,
	-- or the proto-language on a line directly under and at the same level as
	-- its family.
	local family_under_protolanguage = to_boolean(args.famunderproto)
	local protolanguage_under_family = to_boolean(args.protounderfam)
	if family_under_protolanguage and protolanguage_under_family then
		error("Kindly choose between proto-language under family and family under proto-language.")
	end
	
	return export.print_children(descendants_of, {
		hide_families_with_protolanguages = not show_all_families,
		hide_etymology_languages = not show_etymology_languages,
		family_under_protolanguage = family_under_protolanguage,
		protolanguage_under_family = protolanguage_under_family,
		sterile_branch_length = sterile_branch_length,
		collapsed = require("Module:yesno")(args.collapsed),
		lemma_count = lemma_count
	})
end

function export.print_children(descendants_of, options)
	local m_languages = require("Module:languages")
	local m_table = require("Module:table")
	local make_auto_subtabler = require("Module:auto-subtable")
	
	descendants_of = m_languages.getByCode(descendants_of, nil, true, true)
	
	local names = {}
	local protolanguage_of = {}
	
	local children = make_auto_subtabler{}
		
	local descendants = descendants_of:getDescendantCodes()
	table.insert(descendants, descendants_of:getCode())
	
	if descendants_of:hasType("family") then
		protolanguage_of[descendants_of:getCode()] = descendants_of:getProtoLanguageCode()
	end
	
	local memoized = {}
	local get = function(code, func, ...)
		local ret = memoized[code] or func(...)
		if code then
			memoized[code] = ret
		end
		return ret
	end
	
	for _, descendant_code in ipairs(descendants) do
		-- Inner "repeat until true" loop allows break to work like continue, as it will always only run once.
		repeat
			local descendant = get(descendant_code, m_languages.getByCode, descendant_code, nil, true, true)
			names[descendant_code] = descendant:getCanonicalName():gsub("Proto%-", "")
			if descendant:hasType("language") then
				local ancestors = m_table.shallowCopy(descendant:getAncestorCodes())
				local parent_code = descendant:getParentCode()
				if parent_code and descendant:hasType("etymology-only") then
					local parent = get(parent_code, descendant.getParent, descendant)
					if m_table.deepEquals(parent:getAncestorCodes(), ancestors) and
						descendant:getFamilyCode() == parent:getFamilyCode() then
						table.insert(children[parent:getCode()], descendant_code)
						break
					end
				end
				if #ancestors > 0 then
					for _, ancestor in ipairs(ancestors) do
						table.insert(children[ancestor], descendant_code)
					end
					break
				end
			else
				local protolang = descendant:getProtoLanguageCode()
				protolanguage_of[descendant_code] = protolang
				if protolang and descendant:hasAncestor(protolang) then
					table.insert(children[protolang], descendant_code)
					break
				end
			end
			local family_code = descendant:getFamilyCode()
			if family_code then
				local family = get(family_code, descendant.getFamily, descendant)
				local protolang = get(family:getProtoLanguageCode(), family.getProtoLanguage, family)
				if not protolanguage_of[family] then
					protolanguage_of[family] = protolang and protolang:getCode()
				end
				if protolang and protolang:inFamily(family) and protolang:getCode() ~= descendant_code then
					table.insert(children[protolang:getCode()], descendant_code)
				else
					table.insert(children[family:getCode()], descendant_code)
				end
			end
		until true
	end
	
	-- No more auto subtabling needed.
	children = children:un_auto_subtable()
	
	-- Copy to new table, to filter out unwanted ancestors from descendants with multiple ancestors, where some are not descendants of the target language.
	local parent_to_children_map = {}
	for _, code in ipairs(descendants) do
		parent_to_children_map[code] = children[code]
	end
	
	local function make_nested(data, children)
		local make_nil = {}
		for key, val in pairs(data) do
			if type(key) == "number" then
				if children[val] then
					data[val] = make_nested(children[val], children)
					table.insert(make_nil, key)
				end
			else
				data[key] = make_nested(val, children)
			end
		end
		if make_nil[2] then -- Make sure larger keys are removed first.
			table.sort(make_nil, function (a, b) return a > b end)
		end
		for _, key in ipairs(make_nil) do
			table.remove(data, key)
		end
		return data
	end
	
	local nested = make_nested(parent_to_children_map, parent_to_children_map)
	
	local function deep_sort(current)
		local result = {}
		local is_table = {}
		for key, val in pairs(current) do
			if type(key) == "number" then
				table.insert(result, val)
			else
				is_table[key] = true
				table.insert(result, key)
			end
		end
		
		table.sort(result, function(code1, code2)
			return names[code1] < names[code2]
		end)
		
		for i = 1, #result do
			if is_table[result[i]] then
				local name = result[i]
				result[i] = deep_sort(current[result[i]])
				result[i].name = name
			else
				result[i] = { name = result[i] }
			end
		end
		
		return result
	end
	
	nested = deep_sort(nested)
	
	data = { nested = nested, protolanguage_of = protolanguage_of }
	
	local nested_data, protolanguage_of = data.nested, data.protolanguage_of
	
	nested_data = export.find_subtree(nested_data, descendants_of:getCode())
	
	-- Return nil instead of a tree with only the root node.
	if options.must_have_descendants and (nested_data == nil or #nested_data == 0 or nested_data[1] and #nested_data[1] == 0) then
		return nil
	end
	
	local is_protolanguage = {}
	if options.hide_families_with_protolanguages or options.hide_etymology_languages then
		nested_data = filter_nested_data(nested_data, {
			hide_families_with_protolanguages = options.hide_families_with_protolanguages,
			hide_etymology_languages = options.hide_etymology_languages,
		}, protolanguage_of, is_protolanguage)
	end
	
	if not nested_data or not next(nested_data) then
		return nil
	end
	
	local result = {'<div class="familytree"><ul>'}
	local function ins(val)
		table.insert(result, val)
	end
	
	local tree_options = {
		sterile_branch_text = '<span class="familytree-branch">'
			.. ("─"):rep(options.sterile_branch_length or 4)
			.. '</span>',
		family_under_protolanguage = options.family_under_protolanguage,
		protolanguage_under_family = options.protolanguage_under_family,
		lemma_count = options.lemma_count,
	}
	
	local collapsetext, expandtext = 'Collapse', 'Expand'
	for i, subtable in ipairs(nested_data) do
		-- top language name
		ins('<li>')
		
		-- name me!
		local flag = (options.family_under_protolanguage
			or options.protolanguage_under_family)
			and only_child_is_protolanguage(subtable, options, protolanguage_of)
		local top_node = format_node(subtable.name)
		local next_node
		
		if flag then
			subtable = subtable[1]
			next_node = format_node(subtable.name)
			if options.family_under_protolanguage then
				top_node, next_node = next_node, top_node
			end
		end
		
		ins(top_node)
		
		-- top toggle
		local customcollapsible_id = get_customcollapsible_id()
		ins('<span class="familytree-toptoggle mw-customtoggle-'
				.. customcollapsible_id .. '" style="display: none;">')
		ins(options.collapsed and expandtext or collapsetext)
		ins('</span>')
		
		if flag then
			ins('<li>')
			ins(next_node)
		end
		
		-- tree
		ins('<ul class="mw-collapsible')
		if options.collapsed then
			ins(' mw-collapsed')
		end
		ins('" id="mw-customcollapsible-' .. customcollapsible_id)
		ins('" data-expandtext="' .. expandtext)
		ins('" data-collapsetext="' .. collapsetext .. '">')
		ins(make_tree(subtable, is_protolanguage, protolanguage_of, tree_options))
		if flag then
			ins('</li>')
		end
		ins('</ul></li>')
	end
	
	ins('</ul></div>')
	ins(require("Module:TemplateStyles")("Module:family tree/style.css"))
	
	return table.concat(result)
end

return export