模組解[] [] [] []

概要

編輯

{{#invoke:書名 |Syu1meng2 [ |mode=chapter ] }}
{{#invoke:書名 |Zyun1ming4 [ |mode=dotted ] }}
{{#invoke:書名 |Zoek6zung6}}

p .auto_build_citable( 書名, 篇名 );
p .cvs( );
p .ref( );
p .cjk_p( 字串 );
p .array_p( );

用途

編輯

呢個模組係幾個模嘅真身,用來一啲比較難處理嘅中文標點,目前處理嘅有書名號專名號着重號。模組嘅輸出儘量保持同文字式網頁瀏覽器螢幕閱讀器兼容

模組嘅正常輸出係以印刷嘅常規為準,所以就算係排乙式書名號都嘗試會做 kerning,做出來嘅效果係好似書名號有半形版咁。呢種做法係類似標點(例如引號)嘅傳統排法[1]。模組本身並唔會做任何嘅格式化,但係會將輸入加結構同足夠嘅 class,等樣式表可以將輸出格式化。

模組會直接喺 frame 個阿媽(parent)度攞模嘅參數。例外嘅係一個入口(entry point)被兩個或以上模用嘅時候用來認邊個模用緊佢嘅參數會喺 frame 攞,目前呢種參數有書名號嘅 mode=chapter(篇名號模式)同專名號嘅 mode=dotted(翻譯人名模式)。

直接叫用

編輯

其他模組載入呢個模組之後可以直接叫用 auto_build_citable 函數,呢個函數輸入一至兩個字串(可以係 nil),輸出字串,相等於 {{#invoke:書名|Syu1meng2}},但係輸出唔會lang3埋張樣式表。

輔助函數

編輯

其他模組可以載入呢個模組之後用幾個輔助函數,呢幾個係

  • cvs:輸入任何值,輸出字串,係輸入值比較合理嘅字串版本(類似 PostScriptcvs 算子)
  • cjk_p:輸入字串,輸出布林值,係輸入嘅字串似唔似全部係全形中日韓字
  • array_p:輸入仼何值,輸出布林值,係輸入嘅值似唔似係陣列

另外仲有一個 ref 函數可以直接叫用,輸入仼何值,輸出字串,類似 type 算子嘅輸出,但係如果輸入睇落似一個陣列,輸出會係 「array」;如果似 parse_title(一個內部函數)嘅輸出,輸出會係 「parsed-title」。但係因為係唔係 parsed-title 其實係靠估,呢個函數可能唔係太有用。

本地化

編輯

呢個模組係粵維嘅原創(原作者用開嘅 Javascript+PHP 譯做 Lua,再改到喺維基用到),唔係喺第個維基抄返來再本地化。

模組嘅編輯說明

編輯

呢個模組整合咗落引模嘅模組(用來排中文書名)同官網模嘅模組度(用來檢查使唔使喺篇名後便加空格),所以呢個模死咗會整死埋引模同官網模,所以一定唔可以出大錯。發佈之前一定要用 「睇下改咗呢個模之後,用到個模嘅版嘅效果」 嘅功能試清楚冇問題先可以撳發佈掣。

個模組用來排版用嘅樣式表喺模組:書名/styles.css。改完樣式表之後要改埋引模嘅樣式表嘅對應部分。

睇埋

編輯
  1. 參見 。安屏翻譯。敦伯特拉印刷研究所概況文印刷月刊1 (6): 40–47。1931年6月1號。喺2022年10月11號搵到

-- vi:set sw=4 ts=4 ai sm:
---- This module implements the backend logic for displaying the two types
---- of ˈsyˌmiŋˍhou (punctuation mark for indicating the title of a citable
---- work in Chinese languages)
---- It has also been modified to deal with ˍdzœkˍdzuŋˍhou (emphasis marks,
---- which unfortunately needs to be dealt with despite being a standard part
---- of CSS) and also some experimental support for ˈdzynˌmiŋˍhou (punctuation
---- mark for indicating personal and geographical names)

require ('strict');
local p = {};

local TYPE_1 = '甲';
local TYPE_2 = '乙';

--- Auxiliaries ---------------------------------------------------------------

-- Manually count elements, in case # doesn't work for some reason
local function count_elements( thing )
	local it;
	if type(thing) == 'table' then
		it = 0;
		for _, v in pairs(thing) do
			it = it + 1;
		end
	end
	return it;
end

-- Figure out if a thing is an array
local function array_p( thing )
	local it = (type(thing) == 'table');
	if it then
		local i_min, i_max;
		local n = 0;
		for i, v in pairs(thing) do
			if type(i) ~= 'number' then
				it = false;
			else
				if i_min == nil or i < i_min then
					i_min = i;
				end
				if i_max == nil or i > i_max then
					i_max = i;
				end
			end
			n = n + 1;
		end
		-- in PostScript array indexes start at 0, but in Lua they start at 1
		-- in addition, Lua "sequences" cannot contain nils (ignoring for now)
		-- We don't use #thing bc sometimes (e.g., frame.args) it doesn't work
		if it and not (i_min and i_min == 1 and i_max == n) then
			it = false;				-- index has holes, not a real array
		end
	end
	return it;
end

-- Stringify something into a form suitable for debugging and error messages
-- (cvs is the name of the Postscript operator that does this)
local function cvs( s )
	if s == nil or s == false or s == true then
		s= tostring(s);
	elseif type(s) == 'string' then
		s = '(' .. mw.ustring.gsub(s, '([()])', '\\%1') .. ')';
	elseif type(s) == 'function' then
		s = 'FUNCTION';			-- sigh
	elseif type(s) == 'table' then
		local s2 = '';
		if array_p(s) then
			local n = count_elements(s);
			for i = 1, n, 1 do
				if #s2 > 0 then
					s2 = s2 .. ' ';
				end
				s2 = s2 .. cvs(s[i]);
			end
			s2 = '[' .. s2 .. ']';
		else
			for i, v in pairs(s) do
				if #s2 > 1 then
					s2 = s2 .. ' ';
				end
				if type(i) == 'string' and i:match('^[-%w_]+$') then
					s2 = s2 .. '/' .. i;
				else
					s2 = s2 .. cvs(i) .. ' cvn';
				end
				s2 = s2 .. ' ' .. cvs(v);
			end
			s2 = '<<' .. s2 .. '>>';
		end
		s = s2;
	end
	return s;
end

-- Return a sanitized version of an argument value
local function sanitize( arg )
	if arg ~= nil and arg:match('^{{{%w-}}}$') then
		arg = nil
	end
	return arg;
end

-- If the passed string is a null string, turn it into an actual null
local function nullify( s )
	if s == '' then
		s = nil
	end
	return s;
end

-- Check if the given string is (believed to be) CJK
local function cjk_p( s )
	return mw.ustring.match(s, '^['
			.. '—'									-- 2014 (em dash)
			.. '…'									-- 2026
			.. '─'									-- 2500 (line drawing)
			.. '○'									-- 25CB (circle [not zero])
			.. '⺀-䶿'								-- 2E80-4DBF
			.. '一-鿿'								-- 4E00-9FFF
			.. '가-힯'								-- AC00-D7AF
			.. '豈-﫿'								-- F900-FAFF
			.. '︰-﹏'								-- FE30-FE4F
			.. '!-⦆'								-- FF01-FF60
			.. '𠀀-𯨟'								-- 20000-2FA1F
			.. ']+$');
end

local function space_p( c )
	return mw.ustring.match(c, '[  ]');	-- sp, CJK sp
end

local function kernable_left_punctuation_p( c )
	return c and mw.ustring.match(c, '[《〈(【「『]');
end

local function kernable_right_punctuation_p( c )
	return c and mw.ustring.match(c, '[》〉)】」』]');
end

local function left_kernable_narrow_punctuation_p( c )
	return c and mw.ustring.match(c, '[,;:。!]');
end

local function right_kernable_narrow_punctuation_p( c )
	return c and mw.ustring.match(c, '[、,;:。!]');
end

-- Canonicalize the type parameter
local function canonicalize_type_value( type )
	if type == '1' or type == '甲' then
		type = '甲'
	elseif type == '2' or type == '乙' then
		type = '乙'
	end
	return type;
end

-- Try to guess what REALLY is the thing, in the context of this module.
-- Like Lisp or Perl, Lua has no real concept of classes, but unlike Perl
-- there's no way to "bless" an array (table in Lua) into a class.
-- So everything is just a generic table - a most non-ideal situation.
-- (ref is the name of the Perl operator that does this)
local function ref( thing )
	local it = type(thing);
	if thing == nil then
		it = nil
	elseif it == 'table' then
		-- Are we looking at a parsed title?
		if thing.label and type(thing.label) == 'string' then
			it = 'parsed-title';
		-- Are we looking at an array?
		elseif array_p(thing) then
			it = 'array'
		end
	end
	return it;
end

-- Analyze the title and see if it's a link of some kind
local function parse_title( s )
	local it = s;
	local t = ref(s);
	if t == 'string' then
		local label, link, url;
		local bold, italic, tmp;
		bold, tmp = mw.ustring.match(s, "^(''')(.*)%1$");
		if bold then
			s = tmp;
		end
		italic, tmp = mw.ustring.match(s, "^('')(.*)%1$");
		if italic then
			s = tmp;
		end
		link, label = mw.ustring.match(s, '^%[%[%s*([^%[][^%[]*)%s*|%s*([^%[%]]*)%s*%]%]$');
		if link then
			if label and #label == 0 then
				label = link;
			end
		else
			link = mw.ustring.match(s, '^%[%[([^%[]+)%]%]$');
			if link then
				label = link;
			else
				url, label = mw.ustring.match(s, '^%[(%S+)%s([^%]]+)%]$');
				if not url then
					label = s;
				end
			end
		end
		-- Silent change a few things to make it easier to typeset
		label = mw.ustring.gsub(label, '[─]', '—');		-- U+2500 line drawing
		label = mw.ustring.gsub(label, '[○]', '〇');	-- U+25CB circle
		label = mw.ustring.gsub(label, '⸺', '——');	-- 2-em dash -0> 2x em dash'
		it = {
			['label'] = label;
			['link'] = link;
			['url'] = url;
			['bold'] = bold;
			['italic'] = italic;
		};
	elseif t and t ~= 'parsed-title' then
		error('parse-title got unexpected argument '..cvs(s) .. ' (t=' .. cvs(t) .. ')', 2);
	end
	return it;
end

-- Inverse of parse_title: Try to reconstruct a wikified link given its parse
-- This does not seem to actually work
local function linkify_title( label, link, url )
	local it;
	local bold, italic;
	if type(label) == 'table' then
		link = label.link;
		url = label.url;
		bold = label.bold;
		italic = label.italic;
		label = label.label;
	end
	if label == nil then
		-- all is lost
	elseif link ~= nil then
		it = '[[' .. link .. '|' .. label .. ']]';
	elseif url ~= nil then
		it = '[' .. url .. ' ' .. label .. ']';
	else
		it = label;
	end
	if bold then
		it = "'''" .. it .. "'''";
	end
	if italic then
		it = "''" .. it .. "''";
	end
	return it;
end

-- Return a value that should be suitable for use as an alt or aria-label
-- and safe to use for double-quoting
local function build_aria_label_from( s )
	if s == nil then
		return s;
	end
	if type(s) ~= 'string' then
		error('ERROR: build_aria_label got a non-string '..cvs(s), 2);
	end
	return	mw.ustring.gsub(
			mw.ustring.gsub(
			mw.ustring.gsub(
			mw.ustring.gsub(
			mw.ustring.gsub(
			mw.ustring.gsub(s,
				'<[^<>]*>', ''),					-- try to nuke tags
				"'''''", ''),						-- Wikitext bold italic
				"'''", ''),							-- Wikitext bold
				"'", ''),							-- Wikitext italic
				'"', '”'), 
				"'", '’')
end

local function get_token( t )
	local t_next;
	local it = {['pre'] = ''; ['c'] = ''; ['post'] = '';};
	for stage = 1, 3, 1 do
		local formatting = '';
		while true do
			local t1, t2;
			local done_p = false;
			if stage == 1 then
				t1, t2 = mw.ustring.match(t, '^(<[^/][^<>]*>)(.*)$');
			elseif stage == 2 then
				t1 = nil;
			elseif stage == 3 then
				t1, t2 = mw.ustring.match(t, '^(</[^<>]*>)(.*)$');
			else
				error('Internal error 1, stage='..cvs(stage), 1)
			end
			if t1 then
				formatting = formatting .. t1;
				t_next = t2;
			elseif stage == 1 or stage == 3 then
				t1, t2 =  mw.ustring.match(t, "^(''''')(.*)$");
				if not t1 then
					t1, t2 =  mw.ustring.match(t, "^(''')(.*)$")
					if not t1 then
						t1, t2 =  mw.ustring.match(t, "^('')(.*)$");
					end
				end
				if t1 then
					formatting = formatting .. t1;
					t_next = t2;
				else
					done_p = true;
				end
			elseif stage == 2 then
				t1, t2 = mw.ustring.match(t, '^([^<])(.*)$');
				if t1 then
					it.c = it.c .. t1;
					t_next = t2;
				end
				done_p = true;
			else
				error('Internal error 2, stage='..cvs(stage), 1)
			end
		if done_p then break end
			t = t_next;
		end
		if formatting then
			if stage == 1 then
				it.pre = it.pre .. formatting;
			else
				it.post = it.post .. formatting;
			end
		end
	end
	return it, t_next or '';
end

-- Build a type 1 part (not necessarily the entire title)
-- Try to construct the final HTML so that it line wrap correctly
-- while also following kinsokushori rules
local function build_type_1_part( s )
	local it;
	local stage1 = {};
	local opening_p = false;
	s = parse_title(s);
	assert(ref(s) == 'parsed-title')
	it = '';
	local t = s.label;
	while #t > 0 do
		local c, t_next = get_token(t);
		local closing_p = mw.ustring.match('[】》〉)」」』]', c.c);
		if opening_p or closing_p then
			table.insert(stage1[#stage1], c);
		else
			table.insert(stage1, {c});
		end
		opening_p = mw.ustring.match('[【《〈(「「『]', c.c);
		t = t_next;
	end
	for i = 1, #stage1, 1 do
		local stage2 = stage1[i];
		local class = 'zit3';
		if i == #stage1 then
			class = 'hai2zeoi3mei1ge3 '..class;
		end
		if i == 1 then
			class = 'hai2tau4ge3 '..class;
		end
		it = it .. '<span class="'..class..'">' .. '<span class=zit3>';
		for j = 1, #stage2, 1 do
			local class = 'zi6';
			if j == #stage2 then
				class = 'hai2zeoi3mei1ge3 '..class;
			end
			if j == 1 then
				class = 'hai2tau4ge3 '..class;
			end
			local s = stage2[j];
			local c = table.concat({s.pre or '', s.c, s.post or ''});
			it = it .. '<span class="'..class..'">' .. c .. '</span>';
		end
		it = it .. '</span>' .. '</span>';
	end
	--it = 'DEBUG: stage1 = ' .. cvs(stage1) .. '<br>→ it = ' .. cvs(it); 
	s.label = it;
	it = linkify_title(s);
	return it;
end

-- Try to kern the given string
-- The logic is a deterministic finite state machine. The DFA diagram is
-- currently on a piece of paper and will be added here later.
local function kern( s0 )
	local it;
	local s = parse_title(s0);
	assert(ref(s) == 'parsed-title');
	local t = s.label;
	local segment;
	local STATE = { ['INITIAL'] = 'INITIAL';
					['OPENING'] = 'OPENING';
					['CLOSING'] = 'CLOSING';
	};
	local state = STATE.INITIAL;
	local function tag_p( s )
		return s and s:sub(1, 1) == '<';
	end
	local function remember_kerned( c, class, segment, state )
		if segment == nil then
			segment = {};
		end
		table.insert(segment, { ['c'] = c;
								['class'] = class;
								['state'] = state; });
		return segment;
	end
	local function remember_unkerned(c, segment, state)
		if segment == nil then
			segment = {};
		end
		if #segment == 0 
		or segment[#segment].class 
		or segment[#segment].post
		or c.pre then
			table.insert(segment, { ['c'] = c;
									['state'] = state; } );
		else
			segment[#segment].c.c = table.concat({segment[#segment].c.c, c.c});
			segment[#segment].c.post = c.post;
		end
		return segment;
	end
	local function change_class_of_last_remembered( segment, class, state )
		if segment ~= nil and #segment > 0 then
			segment[#segment].class = class;
		end
		return segment;
	end
	local function remove_last_character_from_memory( segment )
		local it = {};
		if segment and #segment > 0 
		and segment[#segment].c.post then	-- last segment has a tag. sigh.
			it.c = '';
		elseif segment and #segment > 0 
		and #(segment[#segment].c.c) > 0 then
			local s1, s2 = mw.ustring.match(segment[#segment].c.c, '^(.*)(.)$');
			it.c = s2;
			it.post = segment[#segment].c.post;
			segment[#segment].c.c = s1;
			segment[#segment].c.post = nil;
			if #s1 == 0 then
				it.pre = segment[#segment].c.pre;
				segment[#segment].c.pre = nil;
			end
		else
			it.c = '';
		end
		return it, segment;
	end
	local function flush_segment( segment, it, state )
		local s = '';
		if segment then
			for i, v in pairs(segment) do
				s = s .. (v.c.pre or '');
				if v.class then
					s = s .. '<span class=' .. v.class .. '>' .. v.c.c .. '</span>'
				else
					s = s .. v.c.c;
				end
				s = s .. (v.c.post or '');
			end
			if #s > 0 then
				s = '<span class=zit3 ' .. '>' .. s .. '</span>';
				--s=mw.ustring.gsub(s,'<','&lt;');--DEBUG
				it = it .. s;
			end
		end
		segment = {};
		return segment, it;
	end
	it = '';
	while #t > 0 do
		-- We might encounter a tag. try to not break it
		-- XXX Actually, if we see a tag maybe we should stop and give up
		local c, t_next = get_token(t);
		if state == STATE.INITIAL then
			if kernable_left_punctuation_p(c.c) then
				state = STATE.OPENING;
				if #it == 0 then
					segment = remember_kerned(c, 'koen1zo2', segment, state);
				else
					segment = remember_kerned(c, 'koen1zo2siu2siu2', segment, state);
				end
			elseif kernable_right_punctuation_p(c.c) then
				state = STATE.CLOSING;
				local c_last;
				c_last, segment = remove_last_character_from_memory(segment);
				segment, it = flush_segment(segment, it, state);
				segment = remember_unkerned(c_last, segment, state);
				segment = remember_kerned(c, 'koen1jau6', segment, state);
			else
				segment = remember_unkerned(c, segment, state);
			end
		elseif state == STATE.OPENING then
			if kernable_left_punctuation_p(c.c) then
				segment = remember_kerned(c, 'koen1zo2', segment, state);
			elseif kernable_right_punctuation_p(c.c) then
				state = STATE.CLOSING;
				segment = remember_kerned(c, 'koen1jau6', segment, state);
			else
				state = STATE.INITIAL;
				segment = remember_unkerned(c, segment, state);
				segment, it = flush_segment(segment, it, state);
			end
		elseif state == STATE.CLOSING then
			if kernable_left_punctuation_p(c.c) then
				state = STATE.OPENING;
				segment, it = flush_segment(segment, it, state);
				segment = remember_unkerned(c, segment, state);
			elseif kernable_right_punctuation_p(c.c) then
				segment = remember_kerned(c, 'koen1jau6', segment, state);
			else
				state = STATE.INITIAL;
				if not space_p(c.c) and not left_kernable_narrow_punctuation_p(c.c) then
					segment = change_class_of_last_remembered(segment, 'koen1jau6siu2siu2', state)
				end
				segment = remember_unkerned(c, segment, state);
				segment, it = flush_segment(segment, it, state);
			end
		else
			error('kern() encountered unexpeected state '.. cvs(state));
		end
		t = t_next;
	end
	segment, it = flush_segment(segment, it, state);
	if ref(s0) == 'parsed-title' then	-- try to return a result that's the same type
		s.label = it;
		it = s;
	end
	return it;
end

-- Build a type 1 citable with type 2 as a fallback
local function build_type_1_citable( work, part, lang )
	local it;
	local part1, part2;
	local class1, class2;
	local prefix, suffix;
	local infix = '';
	work = parse_title(work);
	part = parse_title(part);
	if part ~= nil then
		prefix = '〈';
		suffix = '〉';
		if work ~= nil then
			infix = '・';							-- U+30FB
			part1 = build_type_1_part(work);
			part2 = build_type_1_part(part);
			class1 = 'syu1ming4';
			class2 = 'pin1ming4';
		else
			part1 = build_type_1_part(part);
			class1 = 'pin1ming4';
		end
	elseif work ~= nil then
		part1 = build_type_1_part(work);
		class1 = 'syu1ming4';
		prefix = '《';
		suffix = '》';
	end
	local alt;

	-- build HTML tag with fallback and aria-label
	it = '';
	if part1 ~= nil then
		it = it .. '<span class=' .. class1 .. '>'
				.. '<span class=hoi1>' .. prefix .. '</span>'
				.. linkify_title(part1)
				.. '</span>';
	end
	if part2 ~= nil then
		it = it .. '<span class=' .. class2 .. '>'
				.. '<span class=fan1gaak3>' .. infix .. '</span>'
				.. linkify_title(part2)
				.. '</span>';

		if part1 ~= nil then
			alt = prefix .. build_aria_label_from(part1
												..infix
												..part2) .. suffix;
		else
			alt = prefix .. build_aria_label_from(part2) .. suffix;
		end
	elseif part1 ~= nil then
		alt = prefix .. build_aria_label_from(part1) .. suffix;
	end
	if it ~= nil then
		local attrs = {'<span'};
		it = mw.ustring.gsub(it,
							'(</span>)$',
							'<span class=saan1>' ..suffix .. '</span>%1');
					
		if alt:match('"') then
			error('alt 參數 「' .. alt .. '」 無效,暫時唔支援 ASCII 雙引號');
		end
		table.insert(attrs, 'aria-label="' .. alt .. '"');
		if lang then
			if lang:match('[ "]') then
				error('語言參數 「' .. lang .. '」 無效');
			end
			table.insert(attrs, 'lang="' .. lang .. '"');
		end
		it = table.concat(attrs, ' ') .. '>' .. it .. '</span>';
	end
	return it;
end

-- Build a type 2 citable using the correct quotation marks
-- and, if needed, inner separator
local function build_type_2_citable( work, part, lang )
	local it;
	local class;
	local prefix;
	local suffix;
	local root;
	local alt;
	work = parse_title(work);
	part = parse_title(part);
	if part ~= nil then
		class = 'pin1ming4';
		if work ~= nil then
			prefix = '《';
			suffix = '》';
			work.label = prefix .. work.label;
			part.label = part.label .. suffix;
			root = linkify_title(kern(work))
				.. '・'								-- dot = U+30FB
				.. linkify_title(kern(part));

			alt = work.label .. '・' .. part.label;
		else
			prefix = '〈';
			suffix = '〉';
			part.label = prefix .. part.label .. suffix;
			root = linkify_title(kern(part));
			alt = part.label;
		end
	elseif work ~= nil then
		class = 'syu1ming4';
		prefix = '《';
		suffix = '》';
		work.label = prefix .. work.label .. suffix;
		root = linkify_title(kern(work));
		alt = work.label;
	end
	if root ~= nil then
		alt = build_aria_label_from(alt);
		--prefix = '<span class=hoi1-adj>' .. prefix .. '</span>';
		--suffix = '<span class=saan1-adj>'.. suffix .. '</span>';
		it = '<span class = "' .. class .. '-b" aria-label="' .. alt .. '">' 
				.. root 
				.. '</span>';
	end
	return it;
end

-- Build a non-citable proper noun using CSS underlining or borders
local function build_noncitable_proper_simple( parts, use_dot_p )
	local it;
	local class = 'zyun1ming4';
	local zwsp = '​';								-- U+200B
	local zwnj = '‌';								-- U+200C
	local infix = '・'								-- dot = U+30FB
	local root;
	local alt;
	if not use_dot_p then
		infix = zwnj;
	end
	if parts then
		for k, v in pairs(parts) do
			local part = parse_title(parts[k]);
			if not root then
				root = '';
				alt = '';
			else
				root = root .. zwnj;
				alt = alt .. infix;
			end
			local segment = linkify_title(part);
			alt = build_aria_label_from(part.label);
			segment = '<span class = "' .. class .. '-b" aria-label="' .. alt .. '">' 
					.. segment 
					.. '</span>';
			root = root .. segment;
		end
	end
	it = '<span class=zyun1ming4-b>'
		.. root
		.. '</span>';
	return it;
end

-- Attempt to build a non-citable proper noun using Unicode
local function build_noncitable_proper_alternate( parts, use_dot_p )
	local it;
	local class = 'zyun1ming4';
	local zwsp = '​';								-- U+200B
	local zwnj = '‌';								-- U+200C
	local infix = '・'								-- dot = U+30FB
	local root;
	local alt;
	if not use_dot_p then
		infix = zwnj;
	end
	if parts then
		for k, v in pairs(parts) do
			local part = parse_title(parts[k]);
			if not root then
				root = '';
				alt = '';
			else
				root = root .. zwnj;
				alt = alt .. infix;
			end
			local segment = build_type_1_part(part);
			alt = build_aria_label_from(part.label);
			segment = '<span class = "' .. class .. '" aria-label="' .. alt .. '">' 
					.. segment 
					.. '</span>';
			root = root .. segment;
		end
	end
	it = '<span class=zyun1ming4>'
		.. root
		.. '</span>';
	return it;
end

-- Analyze the given title(s) and decide if type 1 is safe to use
local function determine_which_type_to_use( work, part )
	local it;
	local det;
	local t = ref(work);
	if t == 'parsed-title' then
		work = work.label;
	elseif t == 'array' then
		work = table.concat(work);
	end
	if type(part) == 'table' then
		part = part.label;
	end
	if work ~= nil and part ~= nil then
		det = work .. part;
	elseif work ~= nil then
		det = work;
	elseif part ~= nil then
		det = part;
	else
		det = '';
	end
	if cjk_p(det) then
		it = TYPE_1;
	else
		it = TYPE_2;
	end
	return it;
end

-- Automatically select whether to build either a type 1 or type 2 citable
local function auto_build_citable( work, part, lang )
	local type = determine_which_type_to_use(work, part);
	local it;
	if type == TYPE_1 then
		it = build_type_1_citable(work, part, lang);
	else
		it = build_type_2_citable(work, part, lang);
	end
	return it;
end

-- Automatically select whether to build noncitable with Unicode or underlining
local function auto_build_noncitable( parts, use_dots_p )
	local type = determine_which_type_to_use(parts);
	local it;
	if type == TYPE_1 then
		it = build_noncitable_proper_alternate(parts, use_dots_p);
	else
		it = build_noncitable_proper_simple(parts, use_dots_p);
	end
	return it;
end

--- Exported, invocable functions ---------------------------------------------

-- Entry point for Template:書名 and Template:篇名
p.Syu1meng2 = function( frame )
	local parent = frame:getParent();
	local chapter_mode_p = (frame.args.mode and frame.args.mode == 'chapter') == true;
	local it;
	local alt = '';
	local styles = 'Module:書名/styles.css';
	local work, part;
	local lang;
	local type;

	-- figure out what is actually being marked up as a citable
	local parts = {};
	local name = parent:getTitle();
	if chapter_mode_p then
		table.insert(parts, '');
		name = '篇名模';
	end
	for k, v in pairs(parent.args) do
		local ps = ' (參數明細:' .. cvs(parent.args) .. ')';
		v = sanitize(v);
		if ref(k) == 'number' then
			assert(v ~= nil);
			table.insert(parts, v);
		elseif k == 'type' or k == '式' then
			if type then
				error(name..'遇到 '..k..' 參數,但係已經指咗 「'..type..'」 式'..ps);
			else
				type = canonicalize_type_value(v);
			end
		elseif (not chapter_mode_p and k == 'title')
			or (chapter_mode_p and k == 'work') then
				
			if nullify(parts[1]) then
				error(name..'遇到 '..k..' 參數,但係已經有 「'..parts[1]..'」'..ps);
			else
				parts[1] = v;
			end
		elseif k == 'chapter'
			or (chapter_mode_p and k == 'title') then
				
			if nullify(parts[2]) then
				error(name..'遇到 '..k..' 參數,但係已經有 「'..parts[2]..'」'..ps);
			else
				if not parts[1] then
					parts[1] = '';
				end
				parts[2] = v;
			end
		elseif k == 'lang' or k == '話' or k == '語言' then
			lang = v;
		else
			error(name..'遇到不明參數 「' .. k .. '」'..ps);
		end
	end
	if #parts == 0 then
		error('冇指定書名或者篇名');
	elseif #parts > 2 then
		error('指定咗太多章名,暫時處理唔到(parts='..cvs(parts)..')');
	end
	work = parse_title(nullify(parts[1]));
	part = parse_title(nullify(parts[2]));

	-- fixup default type
	if type == nil then
		type = 'auto';
	end

	-- build it
	if type == 'auto' then
		it = auto_build_citable(work, part)
	elseif type == TYPE_1 then						-- 甲式 (type 1)
		it = build_type_1_citable(work, part)
	elseif type == TYPE_2 then						-- 乙式 (type 2)
		it = build_type_2_citable(work, part)
	else
		error('唔明'..cvs(type)..'式書名號係乜');
	end

	-- request our style sheet
	it = table.concat ({
			frame:extensionTag ('templatestyles', '', {src=styles}),
			it
		});
	return it;
end

-- Entry point for Template:專名
p.Zyun1ming4 = function( frame )
	local parent = frame:getParent();
	local use_dots_p = (frame.args.mode and frame.args.mode == 'dotted') == true;
	local it;
	local alt = '';
	local styles = 'Module:書名/styles.css';
	local error;
	local parts = {};
	for k, v in pairs(parent.args) do
		if type(k) == 'number' then
			table.insert(parts, v);
		elseif not error then
			error = '專名模遇到不明參數 「' .. k .. '」';
		else
			error = error .. '、「' .. k .. '」';
		end
	end
	if #parts == 0 then
		parts = nil;
	end

	-- build it
	if parts ~= nil then
		it = auto_build_noncitable(parts, use_dots_p);
	else
		if error == nil then
			error = '專名模出錯,搵唔到有乜嘢名';
		end
	end

	if it == nil and error ~= nil then
		it = '<span class=error>' .. error .. '</span>'
			.. '(parts=' .. cvs(parts)
			.. ')'
	end
	
	-- request our style sheet
	it = table.concat ({
			frame:extensionTag ('templatestyles', '', {src=styles}),
			it
		});
	return it;
end

-- Entry point for Template:着重
p.Zoek6zung6 = function( frame )
	local parent = frame:getParent();
	local it;
	local alt = '';
	local styles = 'Module:書名/styles.css';

	local parts = {};
	for k, v in pairs(parent.args) do
		if type(k) == 'number' then
			v = parse_title(v);
			v.alt = build_aria_label_from(v.label);
			table.insert(parts, v);
		else
			error('着重模遇到不明參數 「' .. k .. '」');
		end
	end
	if #parts == 0 then
		error('着重模出錯,搵唔到着重乜嘢');
	end
	
	-- build it
	it = '';
	for i, v in pairs(parts) do
		v = parse_title(v);
		local segment = build_type_1_part(v.label);
		if it ~= nil then
			it = it
				.. '<span class=zoek6zung6 aria-label="' .. v.alt .. '">'
				.. segment
				.. '</span>';
		end
	end

	-- request our style sheet
	it = table.concat ({
			frame:extensionTag ('templatestyles', '', {src=styles}),
			it
		});
	return it;
end

-- Entry point for Template:Kern
p.Kern = function( frame )
	local parent = frame:getParent();
	local it;
	local alt = '';
	local styles = 'Module:書名/styles.css';

	local parts = {};
	for k, v in pairs(parent.args) do
		if type(k) == 'number' then
			v = parse_title(v);
			v.alt = build_aria_label_from(v.label);
			table.insert(parts, v);
		else
			error('Kern 模遇到不明參數 「' .. k .. '」');
		end
	end
	if #parts == 0 then
		error('Kern 模出錯,搵唔到 kern 乜嘢');
	end

	-- build it
	it = '';
	for i, v in pairs(parts) do
		v = parse_title(v);
		local segment = kern(v.label);
		if it ~= nil then
			it = it
				.. '<span class=koen1 aria-label="' .. v.alt .. '">'
				.. segment
				.. '</span>';
		end
	end

	-- request our style sheet
	it = table.concat ({
			frame:extensionTag ('templatestyles', '', {src=styles}),
			it
		});
	return it;
end

--- Non-invocable internal functions exported for other modules to use --------

p.cvs = cvs;
p.ref = ref;
p.cjk_p = cjk_p;
p.kernable_left_punctuation_p = kernable_left_punctuation_p;
p.kernable_right_punctuation_p = kernable_right_punctuation_p;
p.array_p = array_p;
p.determine_which_type_to_use = determine_which_type_to_use;
p.build_type_1_citable = build_type_1_citable;
p.build_type_2_citable = build_type_2_citable;
p.auto_build_citable = auto_build_citable;
return p;