--
-- $Id: luatexko.lua,v 1.19 2011/07/22 15:50:08 nomos Exp $
--

module('luatexko', package.seeall)

luatexbase.provides_module({
    name	= 'luatexko',
    date	= '2011/06/01',
    version	= 0.4,
    description	= 'Korean line-breaking and font-switching',
    author	= 'Dohyun Kim',
    license	= 'Public Domain',
})

local cjkclass = {
    [0x2018] = 1, -- ‘
    [0x201C] = 1, -- “
    [0xFF08] = 1, -- （
    [0xFE35] = 1, -- （ vert
    [0x3014] = 1, -- 〔
    [0xFE39] = 1, -- 〔 vert
    [0xFF3B] = 1, -- ［
    [0xFE47] = 1, -- ［ vert
    [0xFF5B] = 1, -- ｛
    [0xFE37] = 1, -- ｛ vert
    [0x3008] = 1, -- 〈
    [0xFE3F] = 1, -- 〈 vert
    [0x300A] = 1, -- 《
    [0xFE3D] = 1, -- 《 vert
    [0x300C] = 1, -- 「
    [0xFE41] = 1, -- 「 vert
    [0x300E] = 1, -- 『
    [0xFE43] = 1, -- 『 vert
    [0x3010] = 1, -- 【
    [0xFE3B] = 1, -- 【 vert
    [0x3001] = 2, -- 、
    [0xFE11] = 2, -- 、 vert
    [0xFF0C] = 2, -- ，
    [0xFE10] = 2, -- ， vert
    [0x2019] = 2, -- ’
    [0x201D] = 2, -- ”
    [0xFF09] = 2, -- ）
    [0xFE36] = 2, -- ） vert
    [0x3015] = 2, -- 〕
    [0xFE3A] = 2, -- 〕 vert
    [0xFF3D] = 2, -- ］
    [0xFE48] = 2, -- ］ vert
    [0xFF5D] = 2, -- ｝
    [0xFE38] = 2, -- ｝ vert
    [0x3009] = 2, -- 〉
    [0xFE40] = 2, -- 〉 vert
    [0x300B] = 2, -- 》
    [0xFE3E] = 2, -- 》 vert
    [0x300D] = 2, -- 」
    [0xFE42] = 2, -- 」 vert
    [0x300F] = 2, -- 』
    [0xFE44] = 2, -- 』 vert
    [0x3011] = 2, -- 】
    [0xFE3C] = 2, -- 】 vert
    [0x00B7] = 3, -- ·
    [0x30FB] = 3, -- ・
    [0xFF1A] = 3, -- ：
    [0xFE13] = 3, -- ： vert
    [0xFF1B] = 3, -- ；
    [0xFE14] = 3, -- ； vert
    [0x3002] = 4, -- 。
    [0xFE12] = 4, -- 。 vert
    [0xFF0E] = 4, -- ．
    [0x2015] = 5, --  ―
    [0x2026] = 5, -- …
    [0xFE19] = 5, -- … vert
    [0x2025] = 5, -- ‥
    [0xFE30] = 5, -- ‥ vert
    [0xFF1F] = 6, -- ？
    [0xFF01] = 6, -- ！
}

local inhibitxspcode = {
    [0x002D] = 0, -- - hyphen minus
    [0x003C] = 0, -- <
    [0x003E] = 1, -- >
    [0x00B0] = 1,
    [0x2015] = 0,
    [0x2018] = 2,
    [0x2019] = 1,
    [0x201C] = 2,
    [0x201D] = 1,
    [0x2026] = 0,
    [0xFE19] = 0,
    [0x2032] = 1,
    [0x2033] = 1,
    [0x3001] = 1,
    [0xFE11] = 1,
    [0x3002] = 1,
    [0xFE12] = 1,
    [0x3008] = 2,
    [0xFE3F] = 2,
    [0x3009] = 1,
    [0xFE40] = 1,
    [0x300A] = 2,
    [0xFE3D] = 2,
    [0x300B] = 1,
    [0xFE3E] = 1,
    [0x300C] = 2,
    [0xFE41] = 2,
    [0x300D] = 1,
    [0xFE42] = 1,
    [0x300E] = 2,
    [0xFE43] = 2,
    [0x300F] = 1,
    [0xFE44] = 1,
    [0x3010] = 2,
    [0xFE3B] = 2,
    [0x3011] = 1,
    [0xFE3C] = 1,
    [0x3012] = 2, -- 〒
    [0x3014] = 2,
    [0xFE39] = 2,
    [0x3015] = 1,
    [0xFE3A] = 1,
    [0x301C] = 0,
    [0xFF08] = 2,
    [0xFE35] = 2,
    [0xFF09] = 1,
    [0xFE36] = 1,
    [0xFF0C] = 1,
    [0xFE10] = 1,
    [0xFF0E] = 1,
    [0xFF1B] = 1,
    [0xFE14] = 1,
    [0xFF1F] = 1,
    [0xFF3B] = 2,
    [0xFE47] = 2,
    [0xFF3D] = 1,
    [0xFE48] = 1,
    [0xFF5B] = 2,
    [0xFE37] = 2,
    [0xFF5D] = 1,
    [0xFE38] = 1,
    [0xFFE5] = 0,
}

local postbreakpenalty = {
    [0x0023] = 500,
    [0x0024] = 500,
    [0x0025] = 500,
    [0x0026] = 500,
    [0x0028] = 10000,
    [0x003C] = 10000, -- <
    [0x005B] = 10000,
    [0x0060] = 10000,
    [0x2018] = 10000,
    [0x201C] = 10000,
    [0x3008] = 10000,
    [0xFE3F] = 10000,
    [0x300A] = 10000,
    [0xFE3D] = 10000,
    [0x300C] = 10000,
    [0xFE41] = 10000,
    [0x300E] = 10000,
    [0xFE43] = 10000,
    [0x3010] = 10000,
    [0xFE3B] = 10000,
    [0x3014] = 10000,
    [0xFE39] = 10000,
    [0xFF03] = 200,
    [0xFF04] = 200,
    [0xFF05] = 200,
    [0xFF06] = 200,
    [0xFF08] = 10000,
    [0xFE35] = 10000,
    [0xFF3B] = 10000,
    [0xFE47] = 10000,
    [0xFF40] = 10000,
    [0xFF5B] = 10000,
    [0xFE37] = 10000,
}

local prebreakpenalty = {
    [0x0021] = 10000,
    [0x0022] = 10000,
    [0x0027] = 10000,
    [0x0029] = 10000,
    [0x002A] = 500,
    [0x002B] = 500,
    [0x002C] = 10000,
    [0x002D] = 10000,
    [0x002E] = 10000,
    [0x002F] = 500,
    [0x003A] = 10000,
    [0x003B] = 10000,
    [0x003E] = 10000, -- >
    [0x003F] = 10000,
    [0x005D] = 10000,
    [0x00B4] = 10000,
    [0x00B7] = 10000, -- ·
    [0x2013] = 10000, -- –
    [0x2014] = 10000, -- —
    [0x2015] = 10000,
    [0x2019] = 10000,
    [0x201D] = 10000,
    [0x2025] = 250,
    [0xFE30] = 250,
    [0x2026] = 250,
    [0xFE19] = 250,
    [0x2212] = 200,
    [0x3001] = 10000,
    [0xFE11] = 10000,
    [0x3002] = 10000,
    [0xFE12] = 10000,
    [0x3005] = 10000,
    [0x3009] = 10000,
    [0xFE40] = 10000,
    [0x300B] = 10000,
    [0xFE3E] = 10000,
    [0x300D] = 10000,
    [0xFE42] = 10000,
    [0x300F] = 10000,
    [0xFE44] = 10000,
    [0x3011] = 10000,
    [0xFE3C] = 10000,
    [0x3015] = 10000,
    [0xFE3A] = 10000,
    [0x3041] = 150,
    [0x3043] = 150,
    [0x3045] = 150,
    [0x3047] = 150,
    [0x3049] = 150,
    [0x3063] = 150,
    [0x3083] = 150,
    [0x3085] = 150,
    [0x3087] = 150,
    [0x308E] = 150,
    [0x309B] = 10000,
    [0x309C] = 10000,
    [0x30A1] = 150,
    [0x30A3] = 150,
    [0x30A5] = 150,
    [0x30A7] = 150,
    [0x30A9] = 150,
    [0x30C3] = 150,
    [0x30E3] = 150,
    [0x30E5] = 150,
    [0x30E7] = 150,
    [0x30EE] = 150,
    [0x30F5] = 150,
    [0x30F6] = 150,
    [0x30FB] = 10000,
    [0x30FC] = 10000,
    [0xFF01] = 10000,
    [0xFF09] = 10000,
    [0xFE36] = 10000,
    [0xFF0B] = 200,
    [0xFF0C] = 10000,
    [0xFE10] = 10000,
    [0xFF0E] = 10000,
    [0xFF1A] = 10000,
    [0xFE13] = 10000,
    [0xFF1B] = 10000,
    [0xFE14] = 10000,
    [0xFF1D] = 200,
    [0xFF1F] = 10000,
    [0xFF3D] = 10000,
    [0xFE48] = 10000,
    [0xFF5D] = 10000,
    [0xFE38] = 10000,
}

local xspcode = {
    [0x0027] = 2,
    [0x0028] = 1,
    [0x0029] = 2,
    [0x002C] = 2,
    [0x002E] = 2,
    [0x003B] = 2,
    [0x005B] = 1,
    [0x005D] = 2,
    [0x0060] = 1,
}

local cjk_glue_spec = {
    [0] =
    {[0] = nil,		{.5,.5},    nil,	{.25,.25},  nil,	nil,	    nil,	nil,	    nil,	nil,	    nil,	},
    {[0] = nil,		nil,	    nil,	{.25,.25},  nil,	nil,	    nil,	nil,	    nil,	nil,	    nil,	},
    {[0] = {.5,.5},	{.5,.5},    nil,	{.25,.25},  nil,	{.5,.5},    {.5,.5},	{.5,.5},    {.5,.5},	{.5,.5},    {.5,.5},	},
    {[0] = {.25,.25},	{.25,.25},  {.25,.25},	{.5,.25},   {.25,.25},	{.25,.25},  {.25,.25},	{.25,.25},  {.25,.25},	{.25,.25},  {.25,.25},	},
    {[0] = {.5,0},	{.5,0},	    nil,	{.75,.25},  nil,	{.5,0},	    {.5,0},	{.5,0},	    {.5,0},	{.5,0},	    {.5,0},	},
    {[0] = nil,		{.5,.5},    nil,	{.25,.25},  nil,	nil,	    nil,	nil,	    nil,	nil,	    nil,	},
    {[0] = {.5,.5},	{.5,.5},    nil,	{.25,.25},  nil,	nil,	    nil,	{.5,.5},    {.5,.5},	{.5,.5},    {.5,.5},	},
    --
    {[0] = nil,		{.5,.5},    nil,	{.25,.25},  nil,	nil,	    nil,	nil,	    nil,	nil,	    nil,	},
    {[0] = nil,		{.5,.5},    nil,	{.25,.25},  nil,	nil,	    nil,	nil,	    nil,	nil,	    nil,	},
    {[0] = nil,		{.5,.5},    nil,	{.25,.25},  nil,	nil,	    nil,	nil,	    nil,	nil,	    nil,	},
    {[0] = nil,		{.5,.5},    nil,	{.25,.25},  nil,	nil,	    nil,	nil,	    nil,	nil,	    nil,	},
}

local latin_fullstop = {
    [0x2e] = 1,
    [0x21] = 2,
    [0x3f] = 2,
}

dotemphnode	= {}
rubynode	= {}
ulinebox	= {}
hangulfont	= {}
hangulcount	= 0

-- automatic josa selection

local josa_list = {
    --		리을,   중성,   종성
    [0xAC00] = {0xC774, 0xAC00, 0xC774}, -- 가 = 이, 가, 이
    [0xC740] = {0xC740, 0xB294, 0xC740}, -- 은 = 은, 는, 은
    [0xC744] = {0xC744, 0xB97C, 0xC744}, -- 을 = 을, 를, 을
    [0xC640] = {0xACFC, 0xC640, 0xACFC}, -- 와 = 과, 와, 과
    [0xC73C] = {-1,	-1,	0xC73C}, -- 으(로) =   ,  , 으
    [0xC774] = {0xC774, -1,	0xC774}, -- 이(라) = 이,  , 이
}

local josa_code = {
    [0x30]	= 3, -- 0
    [0x31]	= 1, -- 1
    [0x33]	= 3, -- 3
    [0x36]	= 3, -- 6
    [0x37]	= 1, -- 7
    [0x38]	= 1, -- 8
    [0x4C]	= 1, -- L
    [0x4D]	= 3, -- M
    [0x4E]	= 3, -- N
    [0x6C]	= 1, -- l
    [0x6D]	= 3, -- m
    [0x6E]	= 3, -- n
    [0xFB02]	= 1, -- ﬂ
    [0xFB04]	= 1, -- ﬄ
    ng		= 3,
    ap		= 3,
    up		= 3,
    at		= 3,
    et		= 3,
    it		= 3,
    ot		= 3,
    ut		= 3,
    ok		= 3,
    ic		= 3,
    le		= 1,
    ime		= 3,
    ine		= 3,
    ack		= 3,
    ick		= 3,
    oat		= 2,
    TEX		= 3,
    -- else 2
}

--[[
local hangingpunctuations = {
[0x002C] = 1, -- ,
[0x002E] = 1, -- .
[0x3001] = 1, -- 、
[0x3002] = 1, -- 。
[0xFF0C] = 1, -- ，
[0xFF0E] = 1, -- ．
}
--]]
-- fonts.protrusions.setups['default'][0xFF0C] = { 0, 1 }
-- fonts.protrusions.setups['default'][0xFF0E] = { 0, 1 }

local fontdata		= fonts.ids
local font_define_func	= callback.find("define_font")

local remove_node	= node.remove -- nodes.delete ???
local insert_before	= node.insert_before
local insert_after	= node.insert_after
local copy_node		= node.copy
local traverse_id	= node.traverse_id
local traverse		= node.traverse
local has_attribute	= node.has_attribute
local unset_attribute	= node.unset_attribute
local set_attribute	= node.set_attribute

local finemathattr	= luatexbase.attributes.finemathattr
local cjtypesetattr	= luatexbase.attributes.cjtypesetattr
local dotemphattr	= luatexbase.attributes.dotemphattr
local autojosaattr	= luatexbase.attributes.autojosaattr
local rubyattr		= luatexbase.attributes.rubyattr
local luakoglueattr	= luatexbase.attributes.luakoglueattr
local hangfntattr	= luatexbase.attributes.hangfntattr
local hanjfntattr	= luatexbase.attributes.hanjfntattr
local unicodeattr	= luatexbase.attributes.unicodeattr
local verticalattr	= luatexbase.attributes.verticalattr

local gluenode		= node.id("glue")
local gluespecnode	= node.id("glue_spec")
local glyphnode		= node.id("glyph")
local discnode		= node.id("disc")
local mathnode		= node.id("math")
local hlistnode		= node.id("hlist")
local vlistnode		= node.id("vlist")
local kernnode		= node.id("kern")
local penaltynode	= node.id("penalty")
local rulenode		= node.id("rule")
local whatsitnode	= node.id("whatsit")
local whatsitspecial	= node.subtype("special")

local emsize = 655360

local new_glue = node.new(gluenode)
local new_glue_spec = node.new(gluespecnode)
local new_penalty = node.new(penaltynode)
local new_kern = node.new(kernnode,1)
local new_rule = node.new(rulenode)

local function get_gluenode (w,st,sh)
    local g = copy_node(new_glue)
    local s = copy_node(new_glue_spec)
    s.width, s.stretch, s.shrink = w or 0, st or 0, sh or 0
    g.spec = s
    return g
end

local function get_penaltynode (n)
    local p = copy_node(new_penalty)
    p.penalty = n or 0
    return p
end

local function get_kernnode (n)
    local k = copy_node(new_kern)
    k.kern = n or 0
    return k
end

local function get_rulenode (w,h,d)
    local r = copy_node(new_rule)
    r.width, r.height, r.depth = w or 0, h or 0, d or 0
    return r
end

local function make_luako_glue(...)
    local glue = get_gluenode(...)
    set_attribute(glue,luakoglueattr,1)
    return glue
end

local function is_cjk_k (c)
    return (c >= 0x2E80  and c <= 0x9FFF )
    or (c >= 0xF900  and c <= 0xFAFF )
    or (c >= 0xFE10  and c <= 0xFE1F )
    or (c >= 0xFE30  and c <= 0xFE4F )
    or (c >= 0xFF00  and c <= 0xFFEF )
    or (c >= 0x20000 and c <= 0x2A6DF)
    or (c >= 0x2F800 and c <= 0x2FA1F)
    or  c == 0x00B0  or  c == 0x2015
    --	or  c == 0x2018  or  c == 0x2019
    --	or  c == 0x201C  or  c == 0x201D
    or  c == 0x2026  or  c == 0x2032
    or  c == 0x2033
end

local function is_hanja (c)
    return (c >= 0x3400 and c <= 0x9FFF )
    or (c >= 0xF900 and c <= 0xFAFF )
    or (c >= 0x20000 and c <= 0x2A6DF)
    or (c >= 0x2F800 and c <= 0x2FA1F)
end

local function is_hangul (c)
    return (c >= 0xAC00 and c <= 0xD7A3)
end

local function is_chosong (c)
    return (c >= 0x1100 and c <= 0x115F)
    or (c >= 0xA960 and c <= 0xA97C)
end

local function is_jungjongsong (c)
    return (c >= 0x1160 and c <= 0x11FF)
    or (c >= 0xD7B0 and c <= 0xD7Fb)
    or c == 0x302E	or  c == 0x302F -- tone marks
end

local function get_cjk_class (ch, cjtype)
    if ch then
	if is_hangul(ch) then return 7 end	  -- hangul = 7
	if is_chosong(ch) then return 8 end	  -- jamo LC = 8
	if is_jungjongsong(ch) then return 9 end  -- jamo VL, TC, TM = 9
	local c = is_cjk_k(ch) and 0 or 10    -- hanja = 0; latin = 10
	if cjkclass[ch] then c = cjkclass[ch] end -- cjkclass 1 .. 6
	if cjtype and cjtype == 2 and
	    (ch == 0xFF1F or ch == 0xFF01 or ch == 0xFF1A or ch == 0xFF1B) then
	    c = 4 -- simplified chinese ？ ！
	end
	return c
    end
    return nil
end

local function get_font_table (fid)
    if fid then
	if fontdata[fid] then
	    return fontdata[fid]
	else
	    return font.fonts[fid]
	end
    end
end

local function get_font_char (fid, chr)
    local f = get_font_table(fid)
    return chr and f and f.characters and f.characters[chr]
end

local function get_font_emsize(fid)
    local f = get_font_table(fid)
    return (f and f.parameters and f.parameters.quad) or emsize
end

local function get_font_feature (fid, name)
    local f = get_font_table(fid)
    return f and f.shared and f.shared.features and f.shared.features[name]
end

local function get_char_boundingbox(fid, chr)
    local f = get_font_table(fid)
    local glyphbox = f and f.descriptions
    and f.descriptions[chr] and f.descriptions[chr].boundingbox
    if glyphbox then
	local factor, bbox = f.factor or 655.36, {}
	for i,v in ipairs(glyphbox) do
	    bbox[i] = v * factor
	end
	return bbox
    end
end

local function get_unicode_char(curr)
    -- LV..T 경우!!!
    if is_jungjongsong(curr.char) then return curr.char end
    local uni = has_attribute(curr, unicodeattr)
    if uni then return uni end
    return curr.char
end

--
-- cjk linebreaking and spacing
--

local function get_hlist_class_first (hlist)
    local curr = hlist.head
    while curr do
	if curr.id == glyphnode then
	    local c,f = get_unicode_char(curr), curr.font
	    if c then return c,f end
	elseif curr.id == hlistnode or curr.id == vlistnode then
	    local c,f = get_hlist_class_first(curr)
	    if c then return c,f end
	elseif curr.id == gluenode then
	    if curr.spec and curr.spec.width ~= 0 then return end
	end
	curr = curr.next
    end
end

local function get_hlist_class_last (hlist,prevchar,prevfont)
    local curr = node.slide(hlist.head)
    while curr do
	if curr.id == glyphnode then
	    local c,f = get_unicode_char(curr), curr.font
	    if c then return c,f end
	elseif curr.id == hlistnode or curr.id == vlistnode then
	    local c,f = get_hlist_class_last(curr)
	    if c then return c,f end
	elseif curr.id == gluenode then
	    if curr.spec and curr.spec.width ~= 0 then return end
	end
	curr = curr.prev
    end
    return prevchar, prevfont
end

local function kanjiskip (head,curr)
    insert_before(head,curr,make_luako_glue(0, emsize*0.1, emsize*0.01))
end

local function xkanjiskip (head,curr)
    if has_attribute(curr,finemathattr) == 0 then -- ttfamily
	kanjiskip(head,curr)
    else
	insert_before(head,curr,make_luako_glue(0.25*emsize, emsize*0.15, emsize*0.06))
    end
end

local function interhangulskip (head,curr,currfont,prevfont,was_penalty)
    local width = 0
    local interhangul = get_font_feature(currfont, "interhangul")
    if tonumber(interhangul) and currfont == prevfont then
	width = interhangul * emsize
    end
    if not was_penalty then
	insert_before(head,curr,get_penaltynode(50))
    end
    insert_before(head,curr,make_luako_glue(width, emsize*0.01, emsize*0.01))
end

local function interhanjaskip (head,curr,was_penalty)
    if not was_penalty then
	insert_before(head,curr,get_penaltynode(50))
    end
    insert_before(head,curr,make_luako_glue(0, emsize*0.01, emsize*0.01))
end

local function koreanlatinskip (head,curr,currfont,prevfont,was_penalty)
    local width = 0 -- default: 0em
    if has_attribute(curr,finemathattr) == 1 then -- not ttfamily
	local latincjk = get_font_feature(currfont, "interlatincjk")
	if not tonumber(latincjk) then
	    latincjk = get_font_feature(prevfont, "interlatincjk")
	end
	if tonumber(latincjk) then
	    width = latincjk * emsize
	end
    end
    if not was_penalty then
	insert_before(head,curr,get_penaltynode(50))
    end
    insert_before(head,curr,make_luako_glue(width, emsize*0.01, emsize*0.01))
end

local function cjk_insert_nodes(head,curr,currchar,currfont,prevchar,prevfont)
    local was_penalty = false
    local currentcjtype = has_attribute(curr,cjtypesetattr)
    local p = get_cjk_class(prevchar, currentcjtype)
    local c = get_cjk_class(currchar, currentcjtype)
    if prebreakpenalty[currchar] then
	was_penalty = true
	insert_before(head,curr,get_penaltynode(prebreakpenalty[currchar]))
    elseif postbreakpenalty[prevchar] then
	was_penalty = true
	insert_before(head,curr,get_penaltynode(postbreakpenalty[prevchar]))
    end
    ---[[ kern is a breakpoint if followed by a glue
    if (c and c == 1) or (p and p >= 2 and p <= 6) then was_penalty = true end
    --]]
    if p and c then
	if currentcjtype then
	    if cjk_glue_spec[p] and cjk_glue_spec[p][c] then
		local width   = emsize * cjk_glue_spec[p][c][1]
		local stretch = 0
		local shrink  = emsize * cjk_glue_spec[p][c][2]
		insert_before(head,curr,make_luako_glue(width, stretch, shrink))
	    elseif p < 10 and c < 10 and p ~= 8 and c ~= 9 then
		kanjiskip(head,curr)
	    elseif (p < 10 and c == 10) or (p == 10 and c < 10) then
		if xspcode[currchar] then
		    if xspcode[currchar] % 2 == 1 then
			xkanjiskip(head,curr)
		    end
		elseif xspcode[prevchar] then
		    if xspcode[prevchar] > 1 then
			xkanjiskip(head,curr)
		    end
		elseif inhibitxspcode[currchar] then -- 3, 2
		    if inhibitxspcode[currchar] > 1 then
			xkanjiskip(head,curr)
		    end
		elseif inhibitxspcode[prevchar] then -- 3, 1
		    if inhibitxspcode[prevchar] % 2 == 1 then
			xkanjiskip(head,curr)
		    end
		else
		    xkanjiskip(head,curr)
		end
	    end
	else
	    if (p < 10 and c == 10) or (p == 10 and c < 10) then
		if xspcode[currchar] then
		    if xspcode[currchar] % 2 == 1 then
			koreanlatinskip(head,curr,currfont,prevfont,was_penalty)
		    end
		elseif xspcode[prevchar] then
		    if xspcode[prevchar] > 1 then
			koreanlatinskip(head,curr,currfont,prevfont,was_penalty)
		    end
		elseif inhibitxspcode[currchar] then -- 3, 2
		    if inhibitxspcode[currchar] > 1 then
			koreanlatinskip(head,curr,currfont,prevfont,was_penalty)
		    end
		elseif inhibitxspcode[prevchar] then -- 3, 1
		    if inhibitxspcode[prevchar] % 2 == 1 then
			koreanlatinskip(head,curr,currfont,prevfont,was_penalty)
		    end
		else
		    koreanlatinskip(head,curr,currfont,prevfont,was_penalty)
		end
	    elseif cjk_glue_spec[p] and cjk_glue_spec[p][c] then
		koreanlatinskip(head,curr,currfont,prevfont,was_penalty)
	    elseif p == 7 and c == 7 then
		interhangulskip(head,curr,currfont,prevfont,was_penalty)
	    elseif p < 10 and c < 10 and p ~= 8 and c ~= 9 then
		interhanjaskip(head,curr,was_penalty)
	    end
	end
	if curr.id == glyphnode
	    and has_attribute(curr,finemathattr) == 1 -- not ttfamily
	    and latin_fullstop[currchar] then
	    if p < 10 or latin_fullstop[prevchar] then
		local punctraise = get_font_feature(prevfont, "punctraise")
		punctraise = tonumber(punctraise) and (punctraise*emsize)
		if punctraise then
		    curr.yoffset = curr.yoffset and (curr.yoffset+punctraise) or punctraise
		end
		local latincjk = get_font_feature(prevfont, "interlatincjk")
		latincjk = tonumber(latincjk) and (latincjk*emsize)
		if latincjk then
		    if latin_fullstop[prevchar] then
			if curr.prev and curr.prev.prev and curr.prev.prev.id == kernnode then
			else
			    insert_before(head,curr,get_kernnode(latincjk))
			end
		    end
		    if curr.next and curr.next.id == glyphnode then
			local nextchar = get_unicode_char(curr.next)
			if nextchar then
			    if latin_fullstop[currchar] ~= 1 and (nextchar == 0x201D or nextchar == 0x2019) then
				insert_after(head,curr,get_kernnode(latincjk))
			    end
			end
		    end
		    currfont = prevfont
		end
	    end
	end
    end
    -- for dot emphasis
    if has_attribute(curr,dotemphattr)
	and c ~= 0
	and c ~= 7
	and p ~= 8
	and c ~= 8 then
	unset_attribute(curr,dotemphattr)
    end

    return currchar,currfont
end

local function cjk_spacing_linebreak (head)
    local prevchar,prevfont = nil,nil
    for curr in traverse(head) do
	if has_attribute(curr,finemathattr) then
	    if curr.id == gluenode then
		prevchar,prevfont = nil,nil
		unset_attribute(curr,finemathattr)
	    elseif curr.id == glyphnode then
		emsize = get_font_emsize(curr.font)
		local uni = get_unicode_char(curr)
		if uni then
		    prevchar,prevfont = cjk_insert_nodes(head,curr,uni,curr.font,prevchar,prevfont)
		end
		unset_attribute(curr,finemathattr)
	    elseif curr.id == mathnode then
		local currchar = 0
		if curr.surround and curr.surround > 0 then
		    currchar = 0x4E00
		end
		if curr.subtype == 0 then
		    cjk_insert_nodes(head,curr,currchar,nil,prevchar,prevfont)
		    prevchar,prevfont = nil,nil
		else
		    prevchar,prevfont = currchar,nil
		end
		unset_attribute(curr,finemathattr)
	    elseif curr.id == hlistnode or curr.id == vlistnode then
		local firstchr, firstfid = get_hlist_class_first(curr)
		if firstchr then
		    cjk_insert_nodes(head,curr,firstchr,firstfid,prevchar,prevfont)
		end
		prevchar,prevfont = get_hlist_class_last(curr,prevchar,prevfont)
		unset_attribute(curr,finemathattr)
	    end
	else
	    prevchar,prevfont = 0,nil -- treat \verb as latin character.
	end
    end
end

--
-- remove japanese/chinese spaceskip
--

local function remove_cj_spaceskip (head)
    for curr in traverse_id(gluenode,head) do
	local cjattr = has_attribute(curr,cjtypesetattr)
	local prv, nxt = curr.prev, curr.next
	if cjattr and cjattr > 0 and prv and nxt then
	    local prevclass, prevchar, prevfont, nextclass
	    if prv.id == hlistnode or prv.id == vlistnode then
		prevclass = get_hlist_class_last(prv)
	    else
		-- what is this strange kern before \text?? ?
		if prv.id == kernnode and prv.kern == 0 then
		    prv = prv.prev
		end
		if prv.id == glyphnode then
		    prevclass = get_cjk_class(get_unicode_char(prv), cjattr)
		    prevchar, prevfont = prv.char, prv.font
		end
	    end
	    if nxt.id == glyphnode then
		nextclass = get_cjk_class(get_unicode_char(nxt), cjattr)
	    elseif nxt.id == hlistnode or nxt.id == vlistnode then
		nextclass = get_hlist_class_first(nxt)
	    end
	    if (prevclass and prevclass < 10) or (nextclass and nextclass < 10) then
		local subtype = curr.subtype
		if subtype == 13 then -- do not touch on xspaceskip for now
		    remove_node(head,curr)
		else -- before \text?? spaceskip is replaced by glue type 0
		    local round = tex.round
		    local spec = curr.spec
		    local csp = spec and spec.width
		    local cst = spec and spec.stretch
		    local csh = spec and spec.shrink
		    local fp = get_font_table(prevfont)
		    fp = fp and fp.parameters
		    local sp = fp and fp.space
		    local st = fp and fp.space_stretch
		    local sh = fp and fp.space_shrink
		    sp = sp and round(sp)
		    st = st and round(st)
		    sh = sh and round(sh)
		    if prevchar and prevchar >= 65 and prevchar <= 90 then
			st = st and math.floor(st*(999/1000))
			sh = sh and math.floor(sh*(1001/1000))
		    end
		    if sp == csp and st == cst and sh == csh then
			remove_node(head,curr)
		    end
		end
	    end
	end
    end
end

--
-- compress fullwidth punctuations
--

--[[
local function get_hss_glue ()
local hss = get_gluenode(0,65536,65536)
hss.spec.stretch_order = 1
hss.spec.shrink_order  = 1
return hss
end
--]]

-- Returning HEAD is important!
local function compress_fullwidth_punctuations (head)
    for curr in traverse_id(glyphnode,head) do
	local uni = get_unicode_char(curr)
	local class = uni and get_cjk_class(uni, has_attribute(curr, cjtypesetattr))
	local chr = get_font_char(curr.font, curr.char)
	if chr and class and class >= 1 and class <= 4 then
	    local width = curr.width or 655360
--	    if has_attribute(curr, verticalattr) then
--		width = curr.depth or 655360 -- should be deleted.
--	    end
	    emsize = get_font_emsize(curr.font)
	    local ensize = emsize/2
	    local oneoften = emsize/10
	    local bbox = get_char_boundingbox(curr.font, curr.char)
	    if not bbox then return head end --
	    if class == 2 or class == 4 then
		local wd
		if uni == curr.char then
		    wd = (bbox[3] < ensize) and ensize-width or bbox[3]-width+oneoften
		else
		    wd = (bbox[2] > ensize) and ensize-width or -bbox[2]+oneoften
		end
		if chr.right_protruding then
		    -- kern is a breakpoint if followed by a glue
		    insert_after(head, curr, get_kernnode(wd))
		else
		    insert_after(head, curr, get_rulenode(wd))
		end
	    elseif class == 1 then
		local wd
		if uni == curr.char then
		    wd = (width-bbox[1] < ensize) and ensize-width or -bbox[1]+oneoften
		else
		    wd = (width-bbox[4] > ensize) and ensize-width or bbox[4]-width+oneoften
		end
		if chr.left_protruding then
		    head = insert_before(head, curr, get_kernnode(wd))
		else
		    head = insert_before(head, curr, get_rulenode(wd))
		end
	    elseif class == 3 then
		local lwd, rwd
		local quarter, thirdquarter, halfwd = ensize/2, ensize*1.5, width/2
		if uni == curr.char then
		    rwd = (bbox[3] < thirdquarter) and quarter-halfwd or bbox[3]-width
		else
		    rwd = (bbox[2] > quarter) and quarter-halfwd or -bbox[2]
		end
		if uni == curr.char then
		    lwd = (width-bbox[1] < thirdquarter) and quarter-halfwd or -bbox[1]
		else
		    lwd = (width-bbox[4] > quarter) and quarter-halfwd or bbox[4]-width
		end
		if chr.left_protruding then
		    head = insert_before(head, curr, get_kernnode(lwd))
		else
		    head = insert_before(head, curr, get_rulenode(lwd))
		end
		if chr.right_protruding then
		    insert_after (head, curr, get_kernnode(rwd))
		else
		    insert_after (head, curr, get_rulenode(rwd))
		end
	    end
	end
    end
    return head
end

--
-- ruby: pre-linebreak routine
--
local function spread_ruby_box(head,extrawidth)
    for curr in traverse_id(gluenode,head) do
	if curr.spec then
	    if has_attribute(curr,luakoglueattr) then
		curr.spec.width = curr.spec.width + extrawidth
	    else
		head = insert_before(head,curr,get_kernnode(curr.spec.width+extrawidth))
		head = remove_node(head,curr)
	    end
	end
    end
    return head
end

local function spread_ruby_base_box (head)
    for curr in traverse_id(hlistnode,head) do
	local attr = has_attribute(curr,rubyattr)
	local rubyoverlap = attr and rubynode[attr][3]
	if attr and not rubyoverlap then
	    local ruby = rubynode[attr][1]
	    if ruby and ruby.width > curr.width then
		local basehead = curr.head
		local numofglues = node.count(gluenode,basehead)
		local extrawidth = (ruby.width - curr.width)/(numofglues + 1)
		if numofglues > 0 then
		    basehead = spread_ruby_box(basehead,extrawidth)
		end
		local leading  = get_kernnode(extrawidth/2)
		curr.width = ruby.width
		leading.next = basehead
		curr.head = leading
	    end
	end
    end
end

local function get_ruby_side_width (basewidth,rubywidth,adjacent)
    local width,margin = (rubywidth-basewidth)/2, 0
    if adjacent then
	if adjacent.id == glyphnode then
	    if not is_hanja(get_unicode_char(adjacent)) then
		width = (rubywidth-basewidth-emsize)/2
		if width > 0 then
		    margin = emsize/2
		else
		    width,margin = 0,(rubywidth-basewidth)/2
		end
	    end
	end
    end
    return width,margin
end

local function get_ruby_side_kern (head)
    for curr in traverse_id(hlistnode,head) do
	local attr = has_attribute(curr,rubyattr)
	local rubyoverlap = attr and rubynode[attr][3]
	if rubyoverlap then
	    local basewidth = curr.width
	    local rubywidth = rubynode[attr][1].width
	    if curr.width < rubywidth then
		local _,fid = get_hlist_class_first(curr)
		emsize = get_font_emsize(fid)
		local leftwidth,leftmargin = get_ruby_side_width(basewidth,rubywidth,curr.prev)
		if leftwidth > 0 then
		    curr.width = curr.width + leftwidth
		end
		if leftmargin > 0 then
		    rubynode[attr].leftmargin = leftmargin
		end
		local rightwidth,rightmargin = get_ruby_side_width(basewidth,rubywidth,curr.next)
		if rightwidth > 0 then
		    curr.width = curr.width + rightwidth
		end
		if rightmargin > 0 then
		    rubynode[attr].rightmargin = rightmargin
		end
		rubynode[attr].rightshift = rightwidth - leftwidth
		local totalspace = leftwidth+rightwidth
		if totalspace > 0 then
		    local numofglues = node.count(gluenode,curr.head)
		    local extrawidth = totalspace/(numofglues + 1)
		    if numofglues > 0 then
			curr.head = spread_ruby_box(curr.head,extrawidth)
		    end
		    local leading = get_kernnode(extrawidth*(leftwidth/totalspace))
		    leading.next = curr.head
		    curr.head = leading
		end
	    end
	end
    end
end

local function zero_width_rule_with_dir (head,curr,before)
    local rule = get_rulenode(0)
    rule.dir = curr.dir
    if before then
	head = insert_before(head,curr,rule)
    else
	insert_after(head,curr,rule)
    end
    return head
end

local function no_ruby_at_margin(head)
    for curr in traverse_id(hlistnode,head) do
	local attr = has_attribute(curr,rubyattr)
	if attr then
	    local margin = rubynode[attr].leftmargin
	    if margin then
		head = insert_before(head,curr,get_kernnode(-margin))
		head = zero_width_rule_with_dir(head,curr,true) -- before
		head = insert_before(head,curr,get_kernnode(margin))
	    end
	    margin = rubynode[attr].rightmargin
	    if margin then
		if curr.next then
		    if curr.next.id == gluenode then
			insert_after(head,curr,get_kernnode(-margin))
			head = zero_width_rule_with_dir(head,curr)
			insert_after(head,curr,get_kernnode(margin))
		    elseif curr.next.id == penaltynode and curr.next.penalty < 10000 then
			insert_after(head,curr.next,get_kernnode(-margin))
			head = zero_width_rule_with_dir(head,curr)
			insert_after(head,curr,get_kernnode(margin))
		    end
		end
	    end
	end
    end
    return head
end

--
-- discourage character widow (고아글자 회피)
--

local function inject_char_widow_penalty (head,curr,uni,pv,cjattr)
    if uni and prebreakpenalty[uni] ~= 10000 then
	local class = get_cjk_class(uni, cjattr)
	if class and class < 9 then
	    if curr.prev and curr.prev.id == rulenode then
		curr = curr.prev
	    end
	    if curr.prev and curr.prev.id == gluenode then
		curr = curr.prev
	    end
	    if curr.prev and curr.prev.id == penaltynode then
		if curr.prev.penalty < pv then
		    curr.prev.penalty = pv
		end
	    else
		insert_before(head,curr,get_penaltynode(pv))
	    end
	    return true
	end
    end
end

local function discourage_char_widow (head,curr)
    while curr do
	local cjattr = has_attribute(curr,cjtypesetattr)
	local pv =  cjattr and 500 or 5000
	if curr.id == glyphnode then
	    emsize = get_font_emsize(curr.font)
	    local width = curr.width or 0
	    if width >= 2*emsize then return end
	    local uni = get_unicode_char(curr)
	    if inject_char_widow_penalty(head,curr,uni,pv,cjattr) then
		return true
	    end
	elseif curr.id == hlistnode and curr.id == vlistnode then
	    local width = curr.width or 0
	    local uni,fid = get_hlist_class_first(curr)
	    emsize = get_font_emsize(fid)
	    if width >= 2*emsize then return end
	    if inject_char_widow_penalty(head,curr,uni,pv,cjattr) then
		return true
	    end
	end
	if not node.has_field(curr, "prev") then return end
	curr = curr.prev
    end
end

--
-- automatic josa selection (자동조사)
--

local function syllable2jamo (code)
    local code = code - 0xAC00
    local L = 0x1100 + math.floor(code / 588)
    local V = 0x1161 + math.floor((code % 588) / 28)
    local T = 0x11A7 + code % 28
    if T == 0x11A7 then T = nil end
    return L, V, T
end

local function number2josacode (n)
    n = n % 10 + string.byte("0")
    if josa_code[n] then return josa_code[n] end
    return nil -- 2
end

local function latin2josacode (n)
    n = n + string.byte("a")
    if josa_code[n] then return josa_code[n] end
    return nil -- 2
end

for c = 0x2160, 0x216B do -- Ⅰ
    josa_code[c] = number2josacode(c - 0x215F)
end
for c = 0x2170, 0x217B do -- ⅰ
    josa_code[c] = number2josacode(c - 0x216F)
end
for c = 0x2460, 0x2473 do -- ①
    josa_code[c] = number2josacode(c - 0x245F)
end
for c = 0x2474, 0x2487 do -- ⑴
    josa_code[c] = number2josacode(c - 0x2473)
end
for c = 0x2488, 0x249B do -- ⒈
    josa_code[c] = number2josacode(c - 0x2487)
end
for c = 0x249C, 0x24B5 do -- ⒜
    josa_code[c] = latin2josacode(c - 0x249C)
end
for c = 0x24B6, 0x24CF do --  Ⓐ
    josa_code[c] = latin2josacode(c - 0x24B6)
end
for c = 0x24D0, 0x24E9 do --  ⓐ
    josa_code[c] = latin2josacode(c - 0x24D0)
end
for c = 0x3131, 0x314E do -- ㄱ
    josa_code[c] = 3
end
josa_code[0x3139] = 1 -- ㄹ
for c = 0x3165, 0x3186 do
    josa_code[c] = 3
end
for c = 0x3200, 0x320D do -- ㈀
    josa_code[c] = 3
end
josa_code[0x3203] = 1 -- ㈃
for c = 0x3260, 0x327F do -- ㉠
    josa_code[c] = 3
end
josa_code[0x3263] = 1 -- ㉣
for c = 0xFF10, 0xFF19 do -- ０
    josa_code[c] = number2josacode(c - 0xFF10)
end
for c = 0xFF21, 0xFF3A do -- Ａ
    josa_code[c] = latin2josacode(c - 0xFF21)
end
for c = 0xFF41, 0xFF5A do -- ａ
    josa_code[c] = latin2josacode(c - 0xFF41)
end

local function jamo2josacode(code)
    if code and code > 0x11A7 then
	if code == 0x11AF then return 1 end
	return 3
    end
    return 2
end

local function get_hanja_hangul_table (table,file,init)
    local i = 0
    local file = kpse.find_file(file)
    if not file then return table end
    file = io.open(file, "r")
    if not file then return table end
    while true do
	local d = file:read("*number")
	if not d then break end
	table[init + i] = d
	i = i + 1
    end
    file:close()
    return table
end

local hanja2hangul = { }
hanja2hangul = get_hanja_hangul_table(hanja2hangul,"hanja_hg.tab",0x4E00)
hanja2hangul = get_hanja_hangul_table(hanja2hangul,"hjexa_hg.tab",0x3400)
hanja2hangul = get_hanja_hangul_table(hanja2hangul,"hjcom_hg.tab",0xF900)

-- 1 : 리을,	2 : 중성,    3 : 종성
local function get_josacode (prevs)
    local code = prevs[1] -- last char
    if not code then return 2 end
    if is_hangul(code) then -- hangul syllable
	local _, _, T = syllable2jamo(code)
	return jamo2josacode(T)
    end
    if is_jungjongsong(code) then return jamo2josacode(code) end
    if (code >= 0x3400 and code <= 0x9FA5)
	or (code >= 0xF900 and code <= 0xFA2D) then
	local _, _, T = syllable2jamo(hanja2hangul[code])
	return jamo2josacode(T)
    end
    -- latin
    if prevs[1] < 0x80
	and prevs[2] and prevs[2] < 0x80
	and prevs[3] and prevs[3] < 0x80 then
	local liii = string.char(prevs[3], prevs[2], prevs[1])
	if josa_code[liii] then return josa_code[liii] end
    end
    if prevs[1] < 0x80
	and prevs[2] and prevs[2] < 0x80 then
	local lii = string.char(prevs[2], prevs[1])
	if josa_code[lii] then return josa_code[lii] end
    end
    if josa_code[code] then return josa_code[code] end
    return 2
end

local function get_josaprevs(curr,josaprev)
    while curr do
	if curr.id == glyphnode then
	    local chr = get_unicode_char(curr)
	    if xspcode[chr]
		or inhibitxspcode[chr]
		or prebreakpenalty[chr]
		or postbreakpenalty[chr]
		or chr == 0x302E	-- tone mark
		or chr == 0x302F then	-- tone mark
		--skip
	    else
		josaprev[#josaprev + 1] = chr
	    end
	elseif curr.id == hlistnode or curr.id == vlistnode then
	    josaprev = get_josaprevs(node.slide(curr.head),josaprev)
	end
	if #josaprev == 3 then break end
	curr = curr.prev
    end
    return josaprev
end

local function korean_autojosa (head)
    for curr in traverse_id(glyphnode,head) do
	if has_attribute(curr,autojosaattr) and has_attribute(curr,finemathattr) then
	    local josaprev = {}
	    josaprev = get_josaprevs(curr.prev,josaprev)
	    local josacode = get_josacode(josaprev)
	    local thischar = get_unicode_char(curr)
	    if thischar == 0xC774 then
		if curr.next
		    and curr.next.id == glyphnode
		    and get_unicode_char(curr.next) == 0xB77C then
		    curr.char = josa_list[0xC774][josacode]
		else
		    curr.char = josa_list[0xAC00][josacode]
		end
	    elseif thischar and josa_list[thischar] then
		curr.char = josa_list[thischar][josacode]
	    end
	end
	if curr.char < 0 then remove_node(head,curr) end
    end
end

-----------------------------------------------------
hanjafontforhanja = false
local hangulfontlist = {}
local function font_substitute(head)
    for curr in traverse_id(glyphnode, head) do
	local eng = get_font_table(curr.font)
	local engsize = eng and eng.size
	local engchr = get_font_char(curr.font, curr.char)
	if engsize and curr.char and not engchr then
	    local korid  = false
	    local hangul = has_attribute(curr, hangfntattr)
	    local hanja  = has_attribute(curr, hanjfntattr)
	    local ftable = {hangul, hanja}
	    if hanjafontforhanja then
		local uni = get_unicode_char(curr)
		uni = uni and get_cjk_class(uni)
		if uni and uni < 7 then ftable = {hanja, hangul} end
	    end
	    for _,fallback in ipairs(ftable) do
		local fkey = fallback and hangulfont[fallback]
		if fkey then
		    local fname, fsize = string.match(fkey, "(.+) at (.+)")
		    if not fname then fname = fkey end
		    if fsize then
			fsize = tex.sp(fsize)
		    else
			fsize = engsize
		    end
		    fkey = fname .. "@" .. fsize
		    local fid = hangulfontlist[fkey]
		    if not fid then
			fid = font_define_func(fname, fsize, font.nextid())
			if type(fid) == "table" then fid = font.define(fid) end
			hangulfontlist[fkey] = fid
		    end
		    local c = get_font_char(fid, curr.char)
		    if c then
			korid = true
			curr.font = fid
			---[[ charraise option charraise
			local charraise = get_font_feature(fid, "charraise")
			emsize = get_font_emsize(fid)
			if tonumber(charraise) then
			    charraise = charraise * emsize
			    curr.yoffset = curr.yoffset and
			    (curr.yoffset + charraise) or charraise
			end
			--]]
			break
		    end
		end
	    end
	    if not korid then
		texio.write_nl(string.format("!Missing character: %s %d",
		unicode.utf8.char(curr.char),curr.char))
	    end
	end
    end
    return head
end
-----------------------------------------------------


--
-- vetical typesetting
--
---[[ maybe to be deleted later
local latin_vertical_fonts_list = {}
local cjk_vertical_fonts_list = {}

local function cjk_vertical_font (id)
    local rf = get_font_table(id)
    if not rf or rf.type == 'virtual' then return end
    local hash = rf.hash and rf.hash..' @ vertical'
    hash = hash or (rf.name and rf.size and rf.name..' @ '..rf.size..' @ vertical')

    if hash then
	local fid = cjk_vertical_fonts_list[hash]
	if fid then return fid end
    else
	return
    end

    local vf = table.fastcopy(rf)
    vf.hash = hash
    vf.type = 'virtual'
    vf.fonts = {{ id = id }}
    local quad = vf.parameters and vf.parameters.quad or 655360
    local descriptions = vf.descriptions
    local ascender = vf.ascender
    local factor = vf.factor
    local xheight = vf.parameters and vf.parameters.x_height or 0
    for i,v in pairs(vf.characters) do
	local vw = descriptions and descriptions[i]
	vw = vw and vw.vwidth
	vw = vw and factor and vw * factor or quad
	local vh = descriptions and descriptions[i]
	vh = vh and vh.boundingbox and vh.boundingbox[3]
	vh = vh and factor and vh * factor
	vh = vh and vh - quad/2 or quad/2
	vh = vh > 0 and vh or nil
	local vd = descriptions and descriptions[i]
	vd = vd and vd.boundingbox and vd.boundingbox[1]
	vd = vd and factor and vd * factor
	vd = vd and quad/2 - vd or quad/2
	vd = vd > 0 and vd or nil
	v.commands = {
	    {'right', ascender},
	    {'down', xheight},
	    {'special', 'pdf: q 0 1 -1 0 0 0 cm'},
	    {'push'},
	    {'char', i},
	    {'pop'},
	    {'special', 'pdf: Q'},
	}
	v.width = vw
	v.height = vh
	v.depth = vd
    end

    local vid = font.define(vf)
    if vid then
	if fontdata[id] and not fontdata[vid] then fontdata[vid] = vf end
	cjk_vertical_fonts_list[hash] = vid
	return vid
    end
end

local function latin_vertical_font (id)
    local rf = get_font_table(id)
    if not rf or rf.type == 'virtual' then return end
    local hash = rf.hash and rf.hash..' @ vertical'
    hash = hash or (rf.name and rf.size and rf.name..' @ '..rf.size..' @ vertical')

    if hash then
	local fid = latin_vertical_fonts_list[hash]
	if fid then return fid end
    else
	return
    end

    local vf = table.fastcopy(rf)
    vf.hash = hash
    vf.type = 'virtual'
    vf.fonts = {{ id = id }}
    local descriptions = vf.descriptions
    local factor = vf.factor
    local halfx = vf.parameters and vf.parameters.x_height
    halfx = halfx and halfx/2 or 0
    for i,v in pairs(vf.characters) do
	local vh = descriptions and descriptions[i]
	vh = vh and vh.boundingbox and vh.boundingbox[4]
	vh = vh and factor and vh * factor
	vh = vh and vh - halfx or (v.height - halfx)
	vh = vh > 0 and vh or nil
	local vd = descriptions and descriptions[i]
	vd = vd and vd.boundingbox and vd.boundingbox[2]
	vd = vd and factor and vd * factor
	vd = vd and halfx - vd or (v.depth + halfx)
	vd = vd > 0 and vd or nil
	v.commands = {
	    {'down', halfx},
	    {'char', i},
	}
	v.height = vh
	v.depth  = vd
    end

    local vid = font.define(vf)
    if vid then
	if fontdata[id] and not fontdata[vid] then fontdata[vid] = vf end
	latin_vertical_fonts_list[hash] = vid
	return vid
    end
end

local initializers = fonts.initializers
function initializers.common.vertical(tfmdata)
    add_vertical_callback()
end
table.insert(fonts.triggers,"vertical")
initializers.node.otf.vertical = initializers.common.vertical

local function vertical_shift (head)
    for curr in traverse(head) do
	if has_attribute(curr, verticalattr) then
	    if curr.id == glyphnode then
		if get_font_feature(curr.font, "vertical") then
		    local fid = cjk_vertical_font(curr.font)
		    if fid then curr.font = fid end
		else
		    local fid = latin_vertical_font(curr.font)
		    if fid then curr.font = fid end
		end
	    elseif curr.id == discnode then
		for _,curr in ipairs({curr.pre, curr.post, curr.replace}) do
		    if curr.id == glyphnode then
			local fid = latin_vertical_font(curr.font)
			if fid then curr.font = fid end
		    end
		end
	    end
	end
    end
    return head
end

vertical_callback_activated = false

function add_vertical_callback ()
    if not vertical_callback_activated then
	luatexbase.add_to_callback('hpack_filter', vertical_shift, 'luatexko.hpack_filter_vertical')
	luatexbase.add_to_callback('pre_linebreak_filter', vertical_shift, 'luatexko.pre_linebreak_vertical')
	vertical_callback_activated = true
    end
end
--]]

-----------------------------------------------------
local function assign_unicode_codevalue (head)
    for curr in traverse_id(glyphnode, head) do
	set_attribute(curr, unicodeattr, curr.char)
    end
    return head
end
-----------------------------------------------------

luatexbase.add_to_callback('hpack_filter',
function(head)
    assign_unicode_codevalue(head)
    korean_autojosa(head)
    remove_cj_spaceskip(head)
    font_substitute(head)
    return head
end, 'luatexko.hpack_filter_first',1)

luatexbase.add_to_callback('hpack_filter',
function(head)
    get_ruby_side_kern(head)
    cjk_spacing_linebreak(head)
    spread_ruby_base_box(head)
    head = compress_fullwidth_punctuations(head)
    -- head = no_ruby_at_margin(head)
    return head
end, 'luatexko.hpack_filter')

luatexbase.add_to_callback('pre_linebreak_filter',
function(head)
    assign_unicode_codevalue(head)
    korean_autojosa(head)
    remove_cj_spaceskip(head)
    font_substitute(head)
    return head
end, 'luatexko.pre_linebreak_filter_first',1)

luatexbase.add_to_callback('pre_linebreak_filter',
function(head)
    get_ruby_side_kern(head)
    cjk_spacing_linebreak(head)
    spread_ruby_base_box(head)
    head = compress_fullwidth_punctuations(head)
    discourage_char_widow(head, node.slide(head))
    head = no_ruby_at_margin(head)
    return head
end, 'luatexko.pre_linebreak_filter')

--
-- dot emphasis (드러냄표 강조)
--
local function after_linebreak_dotemph (head)
    for curr in traverse(head) do
	if curr.id == hlistnode then -- hlist may be nested!!!
	    after_linebreak_dotemph(curr.head)
	elseif curr.id == glyphnode then
	    local attr = has_attribute(curr,dotemphattr)
	    if attr and attr > 0 then
		local d = copy_node(dotemphnode[attr])
		local dot = d.head
		d.head = get_kernnode(-curr.width/2-d.width/2)
		d.head.next = dot
		d.width = 0
		insert_after(head,curr,d)
		unset_attribute(curr,dotemphattr)
	    end
	end
    end
end

--
-- ruby
--
local function after_linebreak_ruby (head)
    for curr in traverse_id(hlistnode,head) do
	after_linebreak_ruby(curr.head) -- hlist may be nested!!!
	local attr = has_attribute(curr,rubyattr)
	if attr then
	    local ruby = rubynode[attr][1]
	    if ruby then
		if ruby.width < curr.width then
		    local rubyhead = ruby.head
		    local numofglues = node.count(gluenode,rubyhead)
		    local extrawidth = (curr.width - ruby.width)/(numofglues + 1)
		    ruby.width = curr.width - extrawidth/2
		    if numofglues > 0 then
			ruby.head = spread_ruby_box(rubyhead,extrawidth)
		    end
		else
		    local right = rubynode[attr].rightshift or 0
		    ruby.width = curr.width + (ruby.width-curr.width+right)/2
		end
		ruby.shift = -curr.height-rubynode[attr][2]
		insert_after(head,curr,ruby)
		insert_after(head,curr,get_kernnode(-ruby.width))
	    end
	    rubynode[attr] = nil
	    unset_attribute(curr,rubyattr)
	end
    end
end

--
-- underline emphasis
--

local function draw_underline(head,curr,width,ulinenum,ulstart)
    if width and width > 0 then
	local glue   = get_gluenode(width)
	glue.subtype = 101 -- cleaders
	glue.leader  = copy_node(ulinebox[ulinenum])
	insert_before(head, curr, get_kernnode(-width))
	insert_before(head, curr, glue)
    end
    for _,nd in ipairs({ulstart,curr}) do
	if nd.id == whatsitnode
	    and nd.subtype == whatsitspecial
	    and nd.data
	    and string.find(nd.data,"luako:uline") then
	    remove_node(head,nd)
	end
    end
end

local function after_linebreak_underline(head,glueorder,glueset,gluesign,ulinenum)
    local ulstart = ulinenum and head or false
    if ulstart and ulstart.id == gluenode then ulstart = ulstart.next end
    for curr in traverse(head) do
	if curr.id == hlistnode then
	    ulinenum = after_linebreak_underline(curr.head,curr.glue_order,curr.glue_set,curr.glue_sign,ulinenum)
	elseif curr.id == whatsitnode and curr.subtype == whatsitspecial
	    and curr.data then
	    if string.find(curr.data,"luako:ulinebegin=") then
		ulinenum = tonumber(string.match(curr.data,"(%d+)"))
		ulstart = curr
	    elseif ulstart and ulinenum
		and string.find(curr.data,'luako:ulineend') then
		local wd = node.dimensions(glueset,gluesign,glueorder,ulstart,curr)
		draw_underline(head,curr,wd,ulinenum,ulstart)
		ulinebox[ulinenum] = nil
		ulinenum = nil
	    end
	end
	if ulstart and ulinenum and curr == node.tail(head) then
	    local wd = node.dimensions(glueset,gluesign,glueorder,ulstart,curr)
	    draw_underline(head,curr,wd,ulinenum,ulstart)
	end
    end
    return ulinenum
end

luatexbase.add_to_callback('vpack_filter',
function(head)
    after_linebreak_dotemph(head)
    after_linebreak_ruby(head)
    after_linebreak_underline(head)
    return true
end, 'luatexko.vpack_filter')

luatexbase.add_to_callback("post_linebreak_filter",
function(head)
    after_linebreak_dotemph(head)
    after_linebreak_ruby(head)
    after_linebreak_underline(head)
    return true
end, 'luatexko.post_linebreak_filter')
