local base = require("packages.base")
local package = pl.class(base)
package._name = "ruby"
local isLatin = function (char)
return (char > 0x20 and char <= 0x24F)
or (char >= 0x300 and char <= 0x36F)
or (char >= 0x1DC0 and char <= 0x1EFF)
or (char >= 0x2C60 and char <= 0x2c7F)
end
local checkIfSpacerNeeded = function (reading)
if not SILE.scratch.lastRubyBox then
return
end
if not isLatin(SU.codepoint(SU.firstChar(reading))) then
return
end
local top = #SILE.typesetter.state.nodes
if top < 2 then
return
end
if
SILE.typesetter.state.nodes[top] ~= SILE.scratch.lastRubyBox
and SILE.typesetter.state.nodes[top - 1] ~= SILE.scratch.lastRubyBox
then
return
end
if not isLatin(SU.codepoint(SU.lastChar(SILE.scratch.lastRubyText))) then
return
end
SILE.typesetter:pushGlue(SILE.settings:get("ruby.latinspacer"))
end
function package:_init ()
base._init(self)
self:loadPackage("font-fallback")
SILE.call("font:add-fallback", { family = "Noto Sans CJK JP" })
SILE.languageSupport.loadLanguage("ja")
end
function package:declareSettings ()
SILE.settings:declare({
parameter = "ruby.height",
type = "measurement",
default = SILE.types.measurement("1zw"),
help = "Vertical offset between the ruby and the main text",
})
SILE.settings:declare({
parameter = "ruby.latinspacer",
type = "glue",
default = SILE.types.node.glue("0.25em"),
help = "Glue added between consecutive Latin ruby",
})
SILE.settings:declare({
parameter = "ruby.opentype",
type = "boolean",
default = true,
help = "Use OpenType tate feature instead of of a bold weight",
})
end
function package:registerCommands ()
self:registerCommand("ruby:font", function (_, _)
if SILE.settings:get("ruby.opentype") then
SILE.call("font", { size = "0.6zw", features = "+ruby" })
else
SILE.call("font", { size = "0.6zw", weight = 700 })
end
end)
self:registerCommand("ruby", function (options, content)
local reading = SU.required(options, "reading", "\\ruby")
SILE.typesetter:setpar("")
checkIfSpacerNeeded(reading)
local rubybox = SILE.call("hbox", {}, function ()
SILE.settings:temporarily(function ()
SILE.call("noindent")
SILE.call("ruby:font")
SILE.typesetter:typeset(reading)
end)
end)
rubybox.outputYourself = function (box, typesetter, line)
local ox = typesetter.frame.state.cursorX
local oy = typesetter.frame.state.cursorY
typesetter.frame:advanceWritingDirection(rubybox.width)
typesetter.frame:advancePageDirection(-SILE.settings:get("ruby.height"))
SILE.outputter:setCursor(typesetter.frame.state.cursorX, typesetter.frame.state.cursorY)
for i = 1, #box.value do
local node = box.value[i]
node:outputYourself(typesetter, line)
end
typesetter.frame.state.cursorX = ox
typesetter.frame.state.cursorY = oy
end
local cbox = SILE.call("hbox", {}, content)
SU.debug("ruby", "base box is", cbox)
SU.debug("ruby", "reading is", rubybox)
if cbox:lineContribution() > rubybox:lineContribution() then
SU.debug("ruby", "Base is longer, offsetting ruby to fit")
rubybox.width = SILE.types.length(cbox:lineContribution() - rubybox:lineContribution()) / 2
else
local diff = rubybox:lineContribution() - cbox:lineContribution()
local to_insert = SILE.types.length(diff / 2)
SU.debug("ruby", "Ruby is longer, inserting", to_insert, "either side of base")
cbox.width = rubybox:lineContribution()
rubybox.height = 0
rubybox.width = 0
table.insert(cbox.value, 1, SILE.types.node.glue(to_insert))
table.insert(cbox.value, SILE.types.node.glue(to_insert))
end
SILE.scratch.lastRubyBox = rubybox
SILE.scratch.lastRubyText = reading
end)
end
package.documentation = [[
\begin{document}
\font:add-fallback[family=Noto Sans CJK JP]
\use[module=packages.ruby]
Japanese texts often contain pronunciation hints (called \em{furigana}) for difficult kanji or foreign words.
These hints are traditionally placed either above (in horizontal typesetting) or beside (in vertical typesetting) the word that they explain.
The typesetting term for these glosses is \em{ruby}.
The \autodoc:package{ruby} package provides the \autodoc:command[check=false]{\ruby[reading=<ruby text>]{<base text>}} command which sets a piece of ruby above or beside the base text.
For example:
\set[parameter=ruby.height,value=12pt]
\define[command=ja]{\font[family=Noto Sans CJK JP,language=ja]{\process}}
\begin{autodoc:codeblock}
\\ruby[reading=\ja{れいわ}]\{\ja{令和}\}
\end{autodoc:codeblock}
Produces:
\medskip
\ja{\ruby[reading=れいわ]{令和}}
\font:remove-fallback
\end{document}
]]
return package