Module:PreDiphic

来自HUIJIA FUN

此模块的文档可以在Module:PreDiphic/doc创建

-- Module:PreDiphic
-- 把罗马字(和Keyman键盘pre_diphic 1.1、Pre-Diphic打字页的打法相同)转换成Pre-Diphic Mono 1.1的私用区字符。
-- 转换规则与MediaWiki:PreDiphicTyper.js的convert()逐条对应。改规则时两边同步。
-- 已经是私用区字符的内容原样保留,所以旧写法也能继续用。

local p = {}

local VO = { a = 0xEE20, e = 0xEE21, o = 0xEE22, i = 0xEE23, u = 0xEE24 }
local CO = {
	p = 0xEE00, b = 0xEE01, t = 0xEE02, d = 0xEE03, k = 0xEE04, g = 0xEE05,
	r = 0xEE09, n = 0xEE0A, f = 0xEE0B, v = 0xEE0C, s = 0xEE0D, z = 0xEE0E,
	l = 0xEE13, m = 0xEE14, q = 0xEE15, j = 0xEE16, x = 0xEE17, y = 0xEE18,
	w = 0xEE19, h = 0xEE1A, c = 0xEE1B,
}
local KO = { r = 0xEE25, l = 0xEE26, n = 0xEE27, m = 0xEE28 }
local PO = { [','] = 0xEE30, ['.'] = 0xEE31, ['!'] = 0xEE32, ['?'] = 0xEE33, ['-'] = 0xEE34 }
local NULLC, RV = 0xEE1F, 0xEE29
-- 双字母辅音:前一个辅音 + 后一个字母 → 合成后的辅音
local DI = {
	[0xEE02] = { s = 0xEE11 },               -- ts
	[0xEE03] = { z = 0xEE12 },               -- dz
	[0xEE0D] = { t = 0xEE0F, h = 0xEE08 },   -- st sh
	[0xEE0E] = { d = 0xEE10, h = 0xEE07 },   -- zd zh
	[0xEE0A] = { g = 0xEE1D },               -- ng
	[0xEE1B] = { h = 0xEE06 },               -- ch
}

local function isV(c)
	return c ~= nil and c >= 0xEE20 and c <= 0xEE24
end
local function isC(c)
	return c ~= nil and c >= 0xEE00 and c <= 0xEE1D
end

-- 罗马字 → 码位数组
function p.codepoints(src)
	local o, syl = {}, false
	for cp in mw.ustring.gcodepoint(mw.ustring.lower(src)) do
		local ch = cp < 128 and string.char(cp) or nil
		local last = o[#o]
		if ch == ' ' then
			if syl then o[#o + 1] = 0x20; syl = false else syl = true end
		elseif ch == '\n' then
			o[#o + 1] = 10; syl = false
		else
			local wasSyl = syl
			syl = false
			if ch and VO[ch] then
				if not wasSyl and (isC(last) or isV(last)) then
					o[#o + 1] = VO[ch]
				else
					o[#o + 1] = NULLC; o[#o + 1] = VO[ch]
				end
			elseif ch and PO[ch] then
				o[#o + 1] = PO[ch]
			elseif ch and ch:match('%d') then
				o[#o + 1] = 0xEE50 + tonumber(ch)
			elseif wasSyl then
				o[#o + 1] = (ch and CO[ch]) or cp
			elseif ch == "'" then
				if isV(last) then o[#o + 1] = last end
			elseif ch == '^' then
				if isC(last) then o[#o + 1] = RV end
			elseif ch == 'h' and #o >= 2 and o[#o - 1] == NULLC and last == 0xEE21 then
				o[#o] = nil; o[#o] = nil; o[#o + 1] = 0xEE1C   -- eh
			elseif ch and last and DI[last] and DI[last][ch] then
				o[#o] = DI[last][ch]
			elseif ch and KO[ch] and (isV(last) or last == RV) then
				o[#o + 1] = KO[ch]
			elseif ch and CO[ch] then
				o[#o + 1] = CO[ch]
			else
				o[#o + 1] = cp
			end
		end
	end
	return o
end

function p.toText(src)
	local o = p.codepoints(src)
	local parts = {}
	for k = 1, #o do parts[k] = mw.ustring.char(o[k]) end
	return table.concat(parts)
end

-- 罗马字的显示形式:音节用连字符,词用空格(yo li  di pi ka → yo-li di-pi-ka)
function p.romanize(src)
	local words = {}
	for w in mw.text.trim(src):gsub('  +', '\1'):gmatch('[^\1]+') do
		words[#words + 1] = (mw.text.trim(w):gsub(' +', '-'))
	end
	return table.concat(words, ' ')
end

-- {{#invoke:PreDiphic|text|罗马字}}:只输出转换后的字符
function p.text(frame)
	return p.toText(frame.args[1] or '')
end

-- 一个码位是否开始一个新的字格(音节、标点、数字串各占一个字高)
-- 元音、韵尾、还原元音(U+EE20–EE29)都挂在前一个音节上,不另占字格
local function startsCell(c)
	return not (c >= 0xEE20 and c <= 0xEE29)
end
local function isDigit(c)
	return c >= 0xEE50 and c <= 0xEE59
end

-- 把一个词(码位数组)切成字格;数字从右往左每三位一格
local function cells(word)
	local out, k = {}, 1
	while k <= #word do
		local c = word[k]
		if isDigit(c) then
			local j = k
			while j + 1 <= #word and isDigit(word[j + 1]) do j = j + 1 end
			local take = (j - k + 1) % 3
			if take == 0 then take = 3 end
			local pos = k
			while pos <= j do
				local cell = {}
				for t = pos, pos + take - 1 do cell[#cell + 1] = word[t] end
				out[#out + 1] = cell
				pos = pos + take
				take = 3
			end
			k = j + 1
		elseif #out == 0 or startsCell(c) then
			out[#out + 1] = { c }
			k = k + 1
		else
			local cell = out[#out]
			cell[#cell + 1] = c
			k = k + 1
		end
	end
	return out
end

-- 按每列字格数分列:只在词界换列;一个词比一列还长时才在词中间断开;换行符另起一列
-- 返回 { {nl=是否新段落, words={ {text=文字, n=字格数, cont=是否接着上一列的半个词}, … }}, … }
function p.columns(src, perCol)
	local cols, cur, used, newPara = {}, nil, 0, true
	local function flush()
		if cur and #cur.words > 0 then cols[#cols + 1] = cur end
		cur, used = nil, 0
	end
	local function open()
		if not cur then cur = { nl = newPara, words = {} }; newPara = false end
	end
	local function piece(cps, n, cont)
		local parts = {}
		for t = 1, #cps do parts[t] = mw.ustring.char(cps[t]) end
		cur.words[#cur.words + 1] = { text = table.concat(parts), n = n, cont = cont }
	end
	local function addWord(word)
		local cs = cells(word)
		if #cs == 0 then return end
		if used > 0 and used + #cs > perCol then flush() end
		local buf, n, cont = {}, 0, false
		for _, cell in ipairs(cs) do
			if used >= perCol then
				open(); piece(buf, n, cont); buf, n, cont = {}, 0, true
				flush()
			end
			open()
			for _, c in ipairs(cell) do buf[#buf + 1] = c end
			n = n + 1
			used = used + 1
		end
		open(); piece(buf, n, cont)
	end
	local word = {}
	for _, c in ipairs(p.codepoints(src)) do
		if c == 0x20 then
			addWord(word); word = {}
		elseif c == 10 then
			addWord(word); word = {}; flush(); newPara = true
		else
			word[#word + 1] = c
		end
	end
	addWord(word)
	flush()
	return cols
end

-- 每个字格的高度约为0.8em(字高800单位)
local CELL_EM = 0.8
local DEFAULT_PER_COL = 16

-- 每列字数:填了「每列」就用它;否则用「高度」÷字格高度换算(两者都是px时);都没填就是16
local function perColumn(args, size)
	local n = tonumber(args['每列'])
	if n and n >= 1 then return math.floor(n) end
	local h = args['高度'] and tonumber((args['高度']:match('^%s*([%d.]+)%s*px%s*$')))
	local fs = size and tonumber((size:match('^%s*([%d.]+)%s*px%s*$')))
	if h and fs and fs > 0 then
		return math.max(1, math.floor(h / (fs * CELL_EM)))
	end
	return DEFAULT_PER_COL
end

-- {{PD}}模板的入口:读模板参数,输出HTML
function p.main(frame)
	local args = frame:getParent().args
	local src = mw.text.trim(args['内容'] or args[1] or '')
	if src == '' then return '' end
	local vertical = args['方向'] ~= '横排'

	local box = mw.html.create('span'):addClass('pd')
	-- 默认竖排、32px;填「方向=横排」改为横排,填「字号=」(留空)则跟随正文
	local size = args['字号']
	if size == nil then size = '32px' end
	if size ~= '' then box:css('font-size', size) end
	if args['颜色'] and args['颜色'] ~= '' then box:css('color', args['颜色']) end

	-- 鼠标悬停显示的罗马字:默认不显示;只有填了「罗马字=…」才显示
	local roman = args['罗马字']
	if roman and roman ~= '' then box:attr('title', roman) end

	if vertical then
		-- 竖排:切成固定字数的列;列从左往右排,撞到正文右边就自动换到下面一排
		box:addClass('pd-cols')
		local maxH = args['高度']
		local fixed = (tonumber(args['每列']) ~= nil) or (maxH ~= nil and maxH ~= '')
		-- 没填「每列」「高度」时,交给Common.js按屏幕高度重新分列(data-pd-auto);
		-- 关掉JS的访客看到的是这里按每列16字分好的结果
		if not fixed then box:attr('data-pd-auto', '1') end
		for _, col in ipairs(p.columns(src, perColumn(args, size))) do
			local c = box:tag('span'):addClass('pd-col')
			if col.nl then c:addClass('pd-nl') end
			-- 「高度」同时作为每列的高度上限(单位不是px、换算不了时也有效)
			if maxH and maxH ~= '' then c:css('max-height', maxH) end
			for k, w in ipairs(col.words) do
				if k > 1 then c:wikitext(' ') end
				local ws = c:tag('span'):addClass('pd-w'):attr('data-n', tostring(w.n))
				if w.cont then ws:attr('data-c', '1') end   -- 接着上一列的半个词
				ws:wikitext(mw.text.nowiki(w.text))
			end
		end
	else
		local lines = mw.text.split(p.toText(src), '\n', true)
		for k, line in ipairs(lines) do
			if k > 1 then box:tag('br') end
			box:wikitext(mw.text.nowiki(line))
		end
	end
	return tostring(box)
end

return p