Module:Family tree

From The Languages of David J. Peterson
Jump to navigation Jump to search

Family tree

This module allows for the display of English Wiktionary's hierarchical language and family ancestry data.

{{#invoke:family tree|show|language code}}

  • The first parameter is the language code.
  • Add |fam=1 (alias |2=) to show language families as separate (often redundant) nodes from their proto-languages. Ordinarily proto-languages are shown without the families that they are the parent of.
  • Add |etym=1 (alias |3=) to show all etymology languages (as children of a "parent" language or language family). Ordinarily they are not shown.
  • Add |famunderproto=1 to show families on a line below the proto-languages that belong to the family and are the common ancestor of its members, and |protounderfam=1 to do the reverse.
  • Add |collapsed=1 to have the top level of the tree collapsed by default.

Lua error at line 254: The language code gem-pro is not a valid non-etymology language or family..

Trees

The template include limit prevents this page from showing all of Wiktionary's current language trees. Above is a sample language tree. When making changes to the Module:languages data, you may need to refresh this page to see an update.

See also


--[=[

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

--]=]

local p = {}

local regular_languages = require("Module:languages/alldata")
local families = require("Module:families/data")

-- Version of [[Module:etymology languages/data]] that chooses the language-
-- 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
		if val.name == code then -- "name" is really code
			return { val }
		else
			local result = p.find_subtree(val, code)
			if result then
				return result
			end
		end
	end
end

local family_icon = "F"
local etymology_language_icon = "E"
local proto_language_icon = family_icon
local family_with_proto_language_icon = family_icon
local function format_node(code, is_protolanguage_or_has_protolanguage)
	local canonical_name, category_name, class, icon, tooltip
	if regular_languages[code] then
		canonical_name = regular_languages[code][1] 
		category_name = canonical_name .. ' language'
		class = "familytree-lang"
		if is_protolanguage_or_has_protolanguage then
			class = class .. ' familytree-protolang'
			icon = proto_language_icon
		end
	elseif etymology_languages[code] then
		canonical_name = etymology_languages[code].canonicalName
		class = "familytree-etymlang"
		icon = etymology_language_icon
		tooltip = "Etymology language"
	elseif families[code] then
		canonical_name = families[code].canonicalName
		category_name = 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 '')
		.. '</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)
	return '</span> ' .. format_node(code,
		is_protolanguage[code] or protolanguage_of[code] ~= nil)
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

p.are_all_children_etymology_languages = require "Module:fun".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 p.are_all_children_etymology_languages(child) == false then
			return false
		end
	end
	
	return true
end)

local customcollapsible_id = 0
local no_break_space = "&nbsp;"
local level_separator = (no_break_space):rep(3)
local expandtext, collapsetext = "[+]─", "[-]┬"
local function make_tree(data, is_protolanguage, protolanguage_of, options, prefix)
	local result = Array()
	
	-- 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)
		
		if not val[1] then
			result:insert('<li>' .. prefix .. branch .. options.sterile_branch_text
				.. language_or_family_node .. '</li>')
		else
			customcollapsible_id = customcollapsible_id + 1
			
			result:insert('<li>' .. prefix .. branch
				.. '<span class="familytree-toggle mw-customtoggle-familytree'
				.. 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)
				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 =
				p.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-familytree' .. customcollapsible_id
				.. '" data-expandtext="' .. expandtext
				.. '" data-collapsetext="' .. collapsetext .. '">'
			
			if flag then
				result:insert(top_node
					.. collapsible_ul .. '<li>' .. prefix
					.. (i == length and no_break_space or "│")
					.. level_separator .. "│")
			end
			
			result:insert(language_or_family_node)
			
			if not flag then
				result:insert(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.
			result:insert(make_tree(val, is_protolanguage, protolanguage_of, options, next_level))
			result:insert('</ul></li>')
		end
	end
	return result:concat()
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 p.show(frame)
	local args = frame.args
	
	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")
	
	-- 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)
	
	-- 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 p.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)
	})
end

function p.print_children(descendants_of, options)
	local data = require("Module:family tree/nested data")
	local nested_data, protolanguage_of = data.nested, data.protolanguage_of
	
	if descendants_of then
		nested_data = p.find_subtree(nested_data, descendants_of)
	end
	
	-- 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
		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 = Array('<div class="familytree"><ul>')
	
	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,
	}
	
	local collapsetext, expandtext = 'Collapse', 'Expand'
	for i, subtable in ipairs(nested_data) do
		-- top language name
		result:insert('<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
		
		result:insert(top_node)
		
		-- top toggle
		customcollapsible_id = customcollapsible_id + 1
		result:insert('<span class="familytree-toptoggle mw-customtoggle-familytree'
				.. customcollapsible_id .. '" style="display: none;">')
		result:insert(options.collapsed and expandtext or collapsetext)
		result:insert('</span>')
		
		if flag then
			result:insert('<li>')
			result:insert(next_node)
		end
		
		-- tree
		result:insert('<ul class="mw-collapsible')
		if options.collapsed then
			result:insert(' mw-collapsed')
		end
		result:insert('" id="mw-customcollapsible-familytree' .. customcollapsible_id)
		result:insert('" data-expandtext="' .. expandtext)
		result:insert('" data-collapsetext="' .. collapsetext .. '">')
		result:insert(make_tree(subtable, is_protolanguage, protolanguage_of, tree_options, nil))
		if flag then
			result:insert('</li>')
		end
		result:insert('</ul></li>')
	end
	
	result:insert('</ul></div>')
	result:insert(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

return p