This module handles ruby texts. It converts between the following 4 forms:

  1. Wikitext
    [[振る|<ruby><rt></rt></ruby>り]][[仮名|<ruby><rt></rt></ruby><ruby><rt></rt></ruby>]]
    
    ()
  2. Texts of the term and the ruby
    [[振る|振り]][[仮%名]]
    
    ふりが%な
    
  3. []() markup
    [[振る|[振](ふ)り]][[仮名|[仮](が)[名](な)]]
    
  4. Lua table, used by modules
    local example_ruby_table = {
        {
            text = {
                {text = '振', ruby = 'ふ'},
                {text = 'り'},
            }, linkto = "振る"
        },
        {
            text = {
                {text = '仮', ruby = 'か'},
                {text = '名', ruby = 'な'},
            }, linkto = "仮名"
        },
    }
    

export.to_text edit

function export.to_text(ruby_table)

Concatenates the texts in a ruby table. Discards all ruby and links.

export.to_ruby edit

function export.to_ruby(ruby_table)

Concatenates the texts in a ruby table. Discards all links. Ruby are used in place of the text below it when present.

export.to_markup edit

function export.to_markup(ruby_table, options)

Generates "[]()" markups from ruby tables.

  • options.break_link = true: Change [[...|<ruby>...<ruby>]] to <ruby>[[...]]<ruby>.
  • options.markup: Use custom markups other than "[...](...)" and "[[...|...]]". Custom markups are not recognized and can not be converted back.

export.to_wiki edit

function export.to_wiki(ruby_table, options)

Generates wikitexts from ruby tables.

  • The options are the same as function export.to_markup()

export.parse_markup edit

function export.parse_markup(markup)

Constructs a ruby table form "[]()" markups.

export.parse_text edit

function export.parse_text(term, kana, options)

Constructs a ruby table from the 2 strings passed to this function. The differences in the 2 strings are converted into ruby, with the corresponding part of kana becoming the ruby text and that of term becoming the text under the ruby. Links in term will be integrated into the result. Links in kana will be ignored by default. Details about the format of term and kana can be found in Template:ja-r/documentation#Ruby_and_transliteration. To better adapt to Japanese texts, this function assumes that all non-letters (except for 5 symbols "^", ".", "-", "゠", "・" for transliteration reasons) and all kana always represent themselves. These literal characters when appearing in term should also appear unchanged or as a hira-kata counterpart in kana. This behaviour can be changed by using options.try, or by manually isolating a single literal character with "%".

  • options.try == nil: Lauch an error when the assumption of literal characters fails.
  • options.try == 'force': Discard the assumption of literal characters when it fails, and try to find any differences in the strings.
  • options.try_force_limit: Limit the time used by options.try == 'force'.
  • options.space == nil: Remove spaces between kana or kanji but preserve elsewhere.
  • options.space == 'all': Preserve all spaces.
  • options.space == 'none': Remove all spaces.
  • options.allow_ruby_link == true: Try to match the links in kana.

export.ruby_auto edit

function export.ruby_auto(args)

A shortcut for combinations like to_wiki(parse_text(...)). It accepts a table containing named arguments instead of positional ones.

  • term, kana: Arguments for parse_text().
  • markup: Argument for parse_markup().
  • to_target: Default to to_wiki(). 'text' for to_text(); 'ruby' for to_ruby(); 'markup' for to_markup().
  • options: A shared option table passed to all functions involved.

local m_str_utils = require("Module:string utilities")

local codepoint = m_str_utils.codepoint
local concat = table.concat
local insert = table.insert
local ipairs = ipairs
local select = select
local type = type
local u = m_str_utils.char
local ufind = m_str_utils.find
local ugmatch = m_str_utils.gmatch
local ugsub = m_str_utils.gsub
local ulen = m_str_utils.len
local umatch = m_str_utils.match
local usub = m_str_utils.sub

local function str_hira_to_kata(s)
    return (ugsub(s, '[ぁ-ゖ]', function(m1) return u(codepoint(m1) + 96) end))
end
local function str_kata_to_hira(s)
    return (ugsub(s, '[ァ-ヶ]', function(m1) return u(codepoint(m1) - 96) end))
end

local export = {}

-- using Wagner–Fischer algorithm
-- str_ucompare is O(nlogn). parse_text is O(n). For long texts, the latter is less prone to Lua timeout error.
local function str_ucompare(s1, s2, limit)
    s1, s2 = mw.text.split(s1, ''), mw.text.split(s2, '')
    local len1, len2 = #s1, #s2
    if limit and len1 * len2 * 20 > limit then return {{s1}, {s2}} end

    local m_cost, m_step = {{0}}, {{}}
    for i = 1, len1 do
        m_cost[i + 1] = {i}
        m_step[i + 1] = {3}
    end
    for j = 1, len2 do
        m_cost[1][j + 1] = j
        m_step[1][j + 1] = 4
    end

    for i = 1, len1 do
        for j = 1, len2 do
            local b_same = s1[i] == s2[j]
            local c_sub = m_cost[i][j] + (b_same and 0 or 1)
            local c_del = m_cost[i][j + 1] + 1
            local c_ins = m_cost[i + 1][j] + 1
            if c_sub <= c_del and c_sub <= c_ins then
                m_cost[i + 1][j + 1] = c_sub
                m_step[i + 1][j + 1] = b_same and 1 or 2
            elseif c_del <= c_ins then
                m_cost[i + 1][j + 1] = c_del
                m_step[i + 1][j + 1] = 3
            else
                m_cost[i + 1][j + 1] = c_ins
                m_step[i + 1][j + 1] = 4
            end
        end
    end

    local i1, i2 = len1 + 1, len2 + 1
    local m_offset = {{-1, -1}, {-1, -1}, {-1, 0}, {0, -1}}
    local r_step_rev = {}
    local r_step_rev_pos1 = {}
    local r_step_rev_pos2 = {}
    local step = m_step[i1][i2]
    while step do
        insert(r_step_rev, step)
        i1 = i1 + m_offset[step][1]
        i2 = i2 + m_offset[step][2]
        insert(r_step_rev_pos1, i1)
        insert(r_step_rev_pos2, i2)
        step = m_step[i1][i2]
    end

    local r1, r2 = {}, {}
    local i = #r_step_rev
    local step = r_step_rev[i]
    while i > 0 do
        local r1_f, r2_f = {}, {}
        if step == 1 then
            repeat
                insert(r1_f, s1[r_step_rev_pos1[i]])
                insert(r2_f, s2[r_step_rev_pos2[i]])
                i = i - 1
                step = r_step_rev[i]
            until step ~= 1
        else
            while true do
                if step == 2 then
                    insert(r1_f, s1[r_step_rev_pos1[i]])
                    insert(r2_f, s2[r_step_rev_pos2[i]])
                elseif step == 3 then
                    insert(r1_f, s1[r_step_rev_pos1[i]])
                elseif step == 4 then
                    insert(r2_f, s2[r_step_rev_pos2[i]])
                else
                    break
                end
                i = i - 1
                step = r_step_rev[i]
            end
        end
        insert(r1, concat(r1_f))
        insert(r2, concat(r2_f))
    end
    return {r1, r2}
end

local function str_parse_link(s)
    local t = {}
    local lt
    local i1, i2
    local i_o = 1
    local i_n = s:find('%[%[', i_o)
    while i_n do
        i1, i2 = s:find('%[%[', i_n + 1), s:find('%]%]', i_n + 2)
        if not i2 then break end
        while i1 and i1 < i2 do
            i_n = i1
            i1 = s:find('%[%[', i_n + 1)
        end
        if i_o < i_n then insert(t, {
            text = s:sub(i_o, i_n - 1),
        }) end
        if i_n + 2 < i2 then
            lt = s:sub(i_n + 2, i2 - 1)
            i1 = lt:find('|')
            if i1 and i1 > 1 and i1 < lt:len() then
                insert(t, {
                    text = lt:sub(i1 + 1),
                    linkto = lt:sub(1, i1 - 1),
                })
            else
                insert(t, {
                    text = lt,
                    linkto = lt,
                })
            end
        end
        i_o = i2 + 2
        i_n = s:find('%[%[', i_o)
    end
    if i_o <= s:len() then insert(t, {
        text = s:sub(i_o),
    }) end
    return t
end

local function table_merge(link_table, ruby_table)
    local r = {}
    local r_sub, r_insert
    local len_cut
    local id_l, id_r = 1, 1
    while id_l <= #link_table and id_r <= #ruby_table do
        len_cut = link_table[id_l].text:len() - ruby_table[id_r].text:len()
        if ruby_table[id_r].ruby and (ruby_table[id_r].ruby:find'%[%[..-%]%]' or len_cut < 0) then
            if ruby_table[id_r].ruby then
                r_sub = {
                    text = {},
                    ruby = str_parse_link(ruby_table[id_r].ruby),
                }
                r_insert = r_sub.text
                insert(r, r_sub)
            else
                r_insert = r
            end
            while len_cut < 0 do
                insert(r_insert, {
                    text = link_table[id_l].text,
                    linkto = link_table[id_l].linkto
                })
                id_l = id_l + 1
                len_cut = len_cut + link_table[id_l].text:len()
            end
            insert(r_insert, {
                text = link_table[id_l].text:sub(1, -1 - len_cut),
                linkto = link_table[id_l].linkto
            })
            if len_cut == 0 then
                id_l = id_l + 1
                id_r = id_r + 1
            else
                link_table[id_l].text = link_table[id_l].text:sub(-len_cut)
                id_r = id_r + 1
            end
        else
            if link_table[id_l].linkto then
                r_sub = {
                    text = {},
                    linkto = link_table[id_l].linkto,
                }
                r_insert = r_sub.text
                insert(r, r_sub)
            else
                r_insert = r
            end
            while len_cut > 0 and not (ruby_table[id_r].ruby and ruby_table[id_r].ruby:find'%[%[..-%]%]') do
                insert(r_insert, {
                    text = ruby_table[id_r].text,
                    ruby = ruby_table[id_r].ruby,
                })
                id_r = id_r + 1
                len_cut = len_cut - ruby_table[id_r].text:len()
            end
            if len_cut == 0 then
                insert(r_insert, {
                    text = ruby_table[id_r].text,
                    ruby = ruby_table[id_r].ruby,
                })
                id_l = id_l + 1
                id_r = id_r + 1
            else
                if ruby_table[id_r].ruby then
                    link_table[id_l].text = link_table[id_l].text:sub(-(len_cut + ruby_table[id_r].text:len()))
                else
                    insert(r_insert, {
                        text = ruby_table[id_r].text:sub(1, -1 + len_cut),
                    })
                    ruby_table[id_r].text = ruby_table[id_r].text:sub(len_cut)
                    id_l = id_l + 1
                end
            end
        end
    end
    return r
end

--[==[Concatenates the texts in a ruby table. Discards all ruby and links.]==]
function export.to_text(ruby_table)
    local r = {}
    local v_text
    for _, v in ipairs(ruby_table) do
        v_text = v.text
        if type(v_text) == 'string' then
            insert(r, v_text)
        else
            insert(r, export.to_text(v_text))
        end
    end
    return concat(r)
end

--[==[Concatenates the texts in a ruby table. Discards all links. Ruby are used in place of the text below it when present.]==]
function export.to_ruby(ruby_table)
    local r = {}
    local v_text
    for _, v in ipairs(ruby_table) do
        v_text = v.ruby or v.text
        if type(v_text) == 'string' then
            insert(r, v_text)
        else
            insert(r, export.to_ruby(v_text))
        end
    end
    return concat(r)
end

local function table_to_markup(ruby_table, break_link, lb, lm, lf, rb, rm, rf)
    local text = {}
    local v_text, v_ruby, v_linkto
    for _, v in ipairs(ruby_table) do
        v_linkto, v_ruby = v.linkto, v.ruby
        if type(v.text) ~= 'string' then
            if break_link and v_linkto then
                v_text = {}
                for _, vv in ipairs(v.text) do
                    if vv.text ~= '' or vv.ruby and vv.ruby ~= '' then
                        insert(v_text, {
                            text = {{
                                text = vv.text,
                                linkto = v_linkto,
                            }},
                            ruby = vv.ruby,
                        })
                    end
                end
                v_linkto, v_ruby = nil, nil
                v_text = table_to_markup(v_text, break_link, lb, lm, lf, rb, rm, rf)
            else
                v_text = table_to_markup(v.text, break_link, lb, lm, lf, rb, rm, rf)
            end
        else
            v_text = v.text
        end
        if v_linkto then
            if v_linkto ~= '' then insert(text, lb .. v_linkto .. lm .. (v_text ~= '' and v_text or '_') .. lf)
            else insert(text, v_text) end
        elseif v_ruby then
            if type(v_ruby) ~= 'string' then v_ruby = table_to_markup(v_ruby, break_link, lb, lm, lf, rb, rm, rf) end
            if v_ruby ~= '' then insert(text, rb .. v_text .. rm .. v_ruby .. rf)
            else insert(text, v_text) end
        else
            insert(text, v_text)
        end
    end
    return concat(text)
end

--[==[Generates {"[]()"} markups from ruby tables.
* {options.break_link = true}: Change {[[...|<ruby>...<ruby>]]} to {<ruby>[[...]]<ruby>}.
* {options.markup}: Use custom markups other than {"[...](...)"} and {"[[...|...]]"}. Custom markups are not recognized and can not be converted back.]==]
function export.to_markup(ruby_table, options)
    options = options or {}
    local omarkup = options.markup or {}

    return table_to_markup(
        ruby_table,
        options.break_link,
        omarkup.link_border_left or '[[',
        omarkup.link_border_middle or '|',
        omarkup.link_border_right or ']]',
        omarkup.ruby_border_left or '[',
        omarkup.ruby_border_middle or '](',
        omarkup.ruby_border_right or ')')
end

--[==[Generates wikitexts from ruby tables.
* The options are the same as {function export.to_markup()}]==]
function export.to_wiki(ruby_table, options)
    options = options or {}
    local omarkup = options.markup or {}

    return table_to_markup(
        ruby_table,
        options.break_link,
        omarkup.link_border_left or '[[',
        omarkup.link_border_middle or '|',
        omarkup.link_border_right or ']]',
        omarkup.ruby_border_left or '<ruby>',
        omarkup.ruby_border_middle or '<rp>(</rp><rt>',
        omarkup.ruby_border_right or '</rt><rp>)</rp></ruby>')
end

--[==[Constructs a ruby table form {"[]()"} markups.]==]
function export.parse_markup(markup)
    local ruby = {}
    local link_table = str_parse_link(markup:gsub('(%b[])(%b())', function(m1, m2)
        insert(ruby, m2:sub(2, -2))
        return m1:sub(2, -2)
    end))

    local plain_text = export.to_text(str_parse_link(markup))
    local ruby_table = {}
    local p0 = 1
    local ruby_n = 1
    local s_text, s_ruby
    for p1, m1, m2, p2 in plain_text:gmatch'()(%b[])(%b())()' do
        if p0 < p1 then
            s_text = plain_text:sub(p0, p1 - 1)
            insert(ruby_table, {text = s_text})
        end
        s_text = m1:sub(2, -2)
        s_ruby = ruby[ruby_n]
        insert(ruby_table, {
            text = s_text,
            ruby = s_ruby ~= '' and s_ruby or nil,
        })
        p0 = p2
        ruby_n = ruby_n + 1
    end
    if p0 <= plain_text:len() then
        s_text = plain_text:sub(p0)
        insert(ruby_table, {text = s_text})
    end

    return table_merge(link_table, ruby_table)
end

local data_range = mw.loadData'Module:ja/data/range'
local range_mute = '%^%-%.゠・'
local range_hirakata = data_range.hiragana .. data_range.katakana
local range_kana = data_range.kana
local range_noalias = '<>^%c%p%s%z' .. range_kana
local range_noruby = range_noalias .. data_range.kana_graph
local range_nospace = range_kana .. data_range.kanji .. data_range.ideograph .. data_range.kana_graph .. data_range.punctuation

--[==[Constructs a ruby table from the 2 strings passed to this function. The differences in the 2 strings are converted into ruby, with the corresponding part of {kana} becoming the ruby text and that of {term} becoming the text under the ruby. Links in {term} will be integrated into the result. Links in {kana} will be ignored by default.
Details about the format of {term} and {kana} can be found in [[Template:ja-r/documentation#Ruby_and_transliteration]].
To better adapt to Japanese texts, this function assumes that all non-letters (except for 5 symbols "^", ".", "-", "゠", "・" for transliteration reasons) and all kana always represent themselves. These literal characters when appearing in {term} should also appear unchanged or as a hira-kata counterpart in {kana}. This behaviour can be changed by using {options.try}, or by manually isolating a single literal character with "%".
* {options.try == nil}: Lauch an error when the assumption of literal characters fails.
* {options.try == 'force'}: Discard the assumption of literal characters when it fails, and try to find any differences in the strings.
* {options.try_force_limit}: Limit the time used by {options.try == 'force'}.
* {options.space == nil}: Remove spaces between kana or kanji but preserve elsewhere.
* {options.space == 'all'}: Preserve all spaces.
* {options.space == 'none'}: Remove all spaces.
* {options.allow_ruby_link == true}: Try to match the links in {kana}.]==]
function export.parse_text(term, kana, options)
	options = options or {}

    local _remove_space
    if options.space == 'none' then
        _remove_space = function(_r)
            local function _next(p1, p2)
                if p2 and p2 < #_r[p1].text then
                    return p1, p2 + 1
                end
                p1 = p1 + 1
                if p1 > #_r then
                    p2 = nil
                else
                    p2 = type(_r[p1].text) ~= 'string' and 1 or nil
                end
                return p1, p2
            end
            local pos1, pos2 = _next(0, nil)
            while pos1 <= #_r do
                local _t = pos2 and _r[pos1].text[pos2] or _r[pos1]
                _t.text = _t.text:gsub(' ', '')
                if _t.linkto then _t.linkto = _remove_space({{text = _t.linkto}})[1].text end
                if _t.ruby then _t.ruby = _remove_space({{text = _t.ruby}})[1].text end
                if pos2 then
                    if _r[pos1].linkto then _r[pos1].linkto = _remove_space({{text = _r[pos1].linkto}})[1].text end
                    if _r[pos1].ruby then _r[pos1].ruby = _remove_space({{text = _r[pos1].ruby}})[1].text end
                end
                pos1, pos2 = _next(pos1, pos2)
            end
            return _r
        end
    elseif options.space == 'all' then
        _remove_space = function(_r)
            return _r
        end
    else
        _remove_space = function(_r, context_ak, context_bk)
            local function _next(p1, p2)
                if p2 and p2 < #_r[p1].text then
                    return p1, p2 + 1
                end
                p1 = p1 + 1
                if p1 > #_r then
                    p2 = nil
                else
                    p2 = type(_r[p1].text) ~= 'string' and 1 or nil
                end
                return p1, p2
            end
            local pos1, pos2 = _next(0, nil)
            local pos3, pos4 = pos1, pos2
            local after_k = context_ak
            local before_k
            local _t, char
            while pos1 <= #_r do
                if pos3 == pos1 and (pos4 == pos2 or pos4 < pos2) or pos3 < pos1 then
                    before_k = context_bk
                    pos3, pos4 = _next(pos1, pos2)
                    while pos3 <= #_r do
                        _t = pos4 and _r[pos3].text[pos4] or _r[pos3]
                        char = ufind(_t.text, '[^ \']')
                        if char then
                            char = usub(_t.text, char, char)
                            before_k = ufind(char, '['..range_nospace..']')
                            break
                        end
                        pos3, pos4 = _next(pos3, pos4)
                    end
                end

                _t = pos2 and _r[pos1].text[pos2] or _r[pos1]
                if _t.linkto then _t.linkto = _remove_space({{text = _t.linkto}}, after_k, before_k)[1].text end
                if _t.ruby then _t.ruby = _remove_space({{text = _t.ruby}}, after_k, before_k)[1].text end
                if pos2 then
                    if _r[pos1].linkto then _r[pos1].linkto = _remove_space({{text = _r[pos1].linkto}}, after_k, before_k)[1].text end
                    if _r[pos1].ruby then _r[pos1].ruby = _remove_space({{text = _r[pos1].ruby}}, after_k, before_k)[1].text end
                end

                local seg = {}
                local i0 = 1
                for i1, m1, i2 in ugmatch(_t.text, '()(['..range_nospace..']+)()') do
                    if after_k and not usub(_t.text, i0, i1 - 1):find'[^ \']' then
                        insert(seg, (usub(_t.text, i0, i1 - 1):gsub(' ', '')))
                    else
                        insert(seg, usub(_t.text, i0, i1 - 1))
                    end
                    insert(seg, m1)
                    after_k = true
                    i0 = i2
                end
                after_k = after_k and not usub(_t.text, i0):find'[^ \']'
                if after_k and before_k then
                    insert(seg, (usub(_t.text, i0):gsub(' ', '')))
                else
                    insert(seg, usub(_t.text, i0))
                end
                _t.text = concat(seg)

                pos1, pos2 = _next(pos1, pos2)
            end
            return _r
        end
    end

	-- Create the link table
	-- e.g. "[[エドガー・アラン・ポー|アラン・ポー]]の[[推理 小説]]"
	local link_table = str_parse_link(term:gsub('%%', '')) -- remove '%'
	--[[link_table = {
        {text = 'アラン・ポー', linkto = 'エドガー・アラン・ポー'},
        {text = 'の'},
        {text = '推理 小説', linkto = '推理 小説'},
    }]]

	-- Remove romaji markup
	kana = kana:gsub('[%^%-%.]', '') -- remove '^', '-', '.', preserve '%', ' '

    -- Create the ruby table
	-- e.g. 'アラン・ポーの推理 小説', 'あらん ぽー の すいり しょうせつ'
	-- ("ぽお" is not allowed)
	local ruby_table = {}
    local plain_term_raw = export.to_text(str_parse_link(term)) -- Remove links: [[A|B]] -> B, [[C]] -> C
    local plain_kana_raw = options.allow_ruby_link and kana or export.to_text(str_parse_link(kana))
	local plain_term = mw.text.split(plain_term_raw, '%%')
	local plain_kana = mw.text.split(plain_kana_raw, '%%')
    if #plain_term ~= #plain_kana then
        mw.logObject(plain_term)
        mw.logObject(plain_kana)
        error('Separator "%" in the kanji and kana strings do not match.')
    end
    for i, plain_term_i in ipairs(plain_term) do
        if plain_term_i ~= '' or plain_kana[i] ~= '' then
            local pattern_ruby, pattern_ruby_is_ruby = {}, {}
            local function _func_pat(s_sub)
                local in_xml_tag = false
                insert(pattern_ruby, '(' .. ugsub(s_sub, '.', function(m0)
                    if in_xml_tag then
                        if m0 == '>' then in_xml_tag = false end
                        return ''
                    else
                        if m0 == '<' then
                            in_xml_tag = true
                            return ' ?<.->'
                        else
                            local m0_m = m0
                            if m0:find'^[%(%)%.%%%+%-%*%?%[%]%^%$]$' then m0_m = '%' .. m0_m end
                            if ufind(m0, '^['..range_mute..']$') then m0_m = '[' .. m0_m .. ' -]?'
                            elseif ufind(m0, '^[ヶゖケ]$') then
                                m0_m = "[" .. str_kata_to_hira(m0_m) .. str_hira_to_kata(m0_m) .. "かがこカガコ]"
                            elseif ufind(m0, '^['..range_hirakata..']$') then
                                m0_m = "[" .. str_kata_to_hira(m0_m) .. str_hira_to_kata(m0_m) .. "]"
                            end
                            return ' ?' .. m0_m
                        end
                    end
                end) .. ' ?)')
            end
            local plain_term_noxml = plain_term_i:gsub('%b<>', '<>')
            local pos0 = 1
            -- Use a custom iterator so that we can exclude "&" and "@" from range_noalias, as they're part of %p.
            for pos1, s, pos2 in (function()
            	local pos2, pos1, c = 1
            	local len = ulen(plain_term_noxml)
            	return function()
            		if pos2 > len then return nil end
					pos1 = math.min(
						ufind(plain_term_noxml, "[^"..range_noalias.."]", pos2) or math.huge,
						ufind(plain_term_noxml, "[&@]", pos2) or math.huge
					)
            		if pos1 == math.huge then return nil end
					pos2 = pos1
					repeat
						pos2, c = select(2, ufind(plain_term_noxml, "(["..range_noalias.."])", pos2 + 1))
					until (not pos2) or (pos2 and not c:find("[&@]"))
					pos2 = pos2 or len + 1
					return pos1, usub(plain_term_noxml, pos1, pos2 - 1), pos2
            	end
            end)() do
                if pos0 < pos1 then
					local s_sub = usub(plain_term_noxml, pos0, pos1 - 1)
                    if not pattern_ruby_is_ruby[#pattern_ruby] or umatch(s_sub, '[^' .. range_mute .. ']') then
                        _func_pat(s_sub)
                    end
                end
                if not pattern_ruby_is_ruby[#pattern_ruby] then
                    insert(pattern_ruby, '(..-)')
                    pattern_ruby_is_ruby[#pattern_ruby] = true
                end
                pos0 = pos2
			end
            if #pattern_ruby == 0 then
                -- isolated symbol matches anything.
                insert(ruby_table, {
					text = plain_term_i,
					ruby = plain_kana[i] ~= plain_term_i and ulen(plain_term_i) == 1 and plain_kana[i] or nil,
				})
            else
				if pos0 <= ulen(plain_term_noxml) then
					_func_pat(usub(plain_term_noxml, pos0))
				end
				local pat_ruby_s = concat(pattern_ruby)
				-- 'アラン・ポーの推理 小説' to '( ?[あア] ?[らラ] ?[んン] ?[・ -]? ?[ぽポ] ?ー ?[のノ] ?)(..-)( )(..-)'
				-- Excute matching
				local ruby_table_i_ruby = {umatch(plain_kana[i], '^'..pat_ruby_s..'$')}
				if #ruby_table_i_ruby > 0 then
					local ruby_table_i_text = {umatch(plain_term_i, '^'..pat_ruby_s..'$')}
					for n_match = 1, #pattern_ruby do
						-- Exclude "&" and "@" from range_noruby, as they're part of %p.
						if (
							pattern_ruby_is_ruby[n_match] and
							ruby_table_i_text[n_match] ~= ruby_table_i_ruby[n_match] and
							(
								ufind(ruby_table_i_text[n_match], '[^' .. range_noruby .. ']') or
								ruby_table_i_text[n_match]:find("[&@]")
							)
						) then
							insert(ruby_table, {
								text = ruby_table_i_text[n_match],
								ruby = ruby_table_i_ruby[n_match],
							})
						else
							if #ruby_table > 0 and ruby_table[#ruby_table].ruby == nil then
								ruby_table[#ruby_table].text = ruby_table[#ruby_table].text .. ruby_table_i_text[n_match]
							else
								insert(ruby_table, {text = ruby_table_i_text[n_match]})
							end
						end
					end
				elseif options.try == 'force' then
					require('Module:debug').track('ja-ruby/forced match')
					local forced_result = str_ucompare(plain_term_i, plain_kana[i], options.try_force_limit)
					for ii, vv in ipairs(forced_result[1]) do
						insert(ruby_table, {
							text = vv,
							ruby = forced_result[2][ii] ~= vv and forced_result[2][ii] or nil,
						})
					end
				else
					mw.log(pat_ruby_s)
					error('Can not match "' .. plain_term_i .. '" and "' .. plain_kana[i] .. '"')
				end
            end
        end
    end
	--[[ruby_table = {
        {text = 'アラン・ポーの'},
        {text = '推理', ruby = 'すいり'},
        {text = ' '}
        {text = '小説', ruby = 'しょうせつ'},
    }]]

    return _remove_space(table_merge(link_table, ruby_table))
	-- Merge the ruby and link table
    --[[return {
        {text = 'アラン・ポー', linkto = 'エドガー・アラン・ポー'},
        {text = 'の'},
        {text = {
            {text = '推理', ruby = 'すいり'},
            {text = ''}
            {text = '小説', ruby = 'しょうせつ'},
        }, linkto = '推理小説'},
    }]]
end

--[==[A shortcut for combinations like {to_wiki(parse_text(...))}. It accepts a table containing named arguments instead of positional ones.
* {term}, {kana}: Arguments for {parse_text()}.
* markup: Argument for {parse_markup()}.
* to_target: Default to {to_wiki()}. {'text'} for {to_text()}; {'ruby'} for {to_ruby()}; {'markup'} for {to_markup()}.
* options: A shared option table passed to all functions involved.]==]
function export.ruby_auto(args)
    local to_target
    if args.target == 'text' then
        to_target = export.to_text
    elseif args.target == 'ruby' then
        to_target = export.to_ruby
    elseif args.target == 'markup' then
        to_target = export.to_markup
    else
        to_target = export.to_wiki
    end

    if args.term and args.kana then
        return to_target(export.parse_text(args.term, args.kana, args.options), args.options)
    elseif args.markup then
        return to_target(export.parse_markup(args.markup, args.options), args.options)
    else
        error('Cannot find "term" and "kana" or "markup"')
    end
end

return export