local nodefactory = require("core.nodefactory")
local hb = require("justenoughharfbuzz")
local ot = require("core.opentype-parser")
local syms = require("packages.math.unicode-symbols")
local atomType = syms.atomType
local symbolDefaults = syms.symbolDefaults
local elements = {}
local mathMode = {
display = 0,
displayCramped = 1,
text = 2,
textCramped = 3,
script = 4,
scriptCramped = 5,
scriptScript = 6,
scriptScriptCramped = 7
}
local scriptType = {
upright = 1,
bold = 2, italic = 3, boldItalic = 4, script = 5,
boldScript = 6,
fraktur = 7,
boldFraktur = 8,
doubleStruck = 9, sansSerif = 10, sansSerifBold = 11, sansSerifItalic = 12,
sansSerifBoldItalic = 13, monospace = 14, }
local mathVariantToScriptType = function(attr)
return
attr == "normal" and scriptType.upright or
attr == "bold" and scriptType.bold or
attr == "italic" and scriptType.italic or
attr == "bold-italic" and scriptType.boldItalic or
attr == "double-struck" and scriptType.doubleStruck or
SU.error("Invalid value \""..attr.."\" for option mathvariant")
end
local function isDisplayMode(mode)
return mode <= 1
end
local function isCrampedMode(mode)
return mode % 2 == 1
end
local function isScriptMode(mode)
return mode == mathMode.script or mode == mathMode.scriptCramped
end
local function isScriptScriptMode(mode)
return mode == mathMode.scriptScript or mode == mathMode.scriptScriptCramped
end
local mathScriptConversionTable = {
capital = {
[scriptType.upright] = function(codepoint) return codepoint end,
[scriptType.bold] = function(codepoint)
return codepoint + 0x1D400 - 0x41
end,
[scriptType.italic] = function(codepoint) return codepoint + 0x1D434 - 0x41 end,
[scriptType.boldItalic] = function(codepoint) return codepoint + 0x1D468 - 0x41 end,
[scriptType.doubleStruck] = function(codepoint)
return codepoint == 0x43 and 0x2102 or
codepoint == 0x48 and 0x210D or
codepoint == 0x4E and 0x2115 or
codepoint == 0x50 and 0x2119 or
codepoint == 0x51 and 0x211A or
codepoint == 0x52 and 0x211D or
codepoint == 0x5A and 0x2124 or
codepoint + 0x1D538 - 0x41
end
},
small = {
[scriptType.upright] = function(codepoint) return codepoint end,
[scriptType.bold] = function(codepoint) return codepoint + 0x1D41A - 0x61 end,
[scriptType.italic] = function(codepoint) return codepoint == 0x68 and 0x210E or codepoint + 0x1D44E - 0x61 end,
[scriptType.boldItalic] = function(codepoint) return codepoint + 0x1D482 - 0x61 end,
[scriptType.doubleStruck] = function(codepoint) return codepoint + 0x1D552 - 0x61 end,
}
}
local mathCache = {}
local function retrieveMathTable(font)
local key = SILE.font._key(font)
if not mathCache[key] then
SU.debug("math", "Loading math font", key)
local face = SILE.font.cache(font, SILE.shaper.getFace)
if not face then
SU.error("Could not find requested font ".. font .." or any suitable substitutes")
end
local mathTable = ot.parseMath(hb.get_table(face, "MATH"))
local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
if mathTable == nil then
SU.error("You must use a math font for math rendering.")
end
local constants = {}
for k, v in pairs(mathTable.mathConstants) do
if type(v) == "table" then v = v.value end
if k:sub(-9) == "ScaleDown" then constants[k] = v / 100
else
constants[k] = v * font.size / upem
end
end
local italicsCorrection = {}
for k, v in pairs(mathTable.mathItalicsCorrection) do
italicsCorrection[k] = v.value * font.size / upem
end
mathCache[key] = {
constants = constants,
italicsCorrection = italicsCorrection,
mathVariants = mathTable.mathVariants,
unitsPerEm = upem
}
end
return mathCache[key]
end
local function getSuperscriptMode(mode)
if mode == mathMode.display or mode == mathMode.text then
return mathMode.script
elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
return mathMode.scriptCramped
elseif mode == mathMode.script or mode == mathMode.scriptScript then
return mathMode.scriptScript
else return mathMode.scriptScriptCramped end
end
local function getSubscriptMode(mode)
if mode == mathMode.display or mode == mathMode.text
or mode == mathMode.displayCramped or mode == mathMode.textCramped then
return mathMode.scriptCramped
else return mathMode.scriptScriptCramped end
end
local function getNumeratorMode(mode)
if mode == mathMode.display then
return mathMode.text
elseif mode == mathMode.displayCramped then
return mathMode.textCramped
elseif mode == mathMode.text then
return mathMode.script
elseif mode == mathMode.textCramped then
return mathMode.scriptCramped
elseif mode == mathMode.script or mode == mathMode.scriptScript then
return mathMode.scriptScript
else return mathMode.scriptScriptCramped end
end
local function getDenominatorMode(mode)
if mode == mathMode.display or mode == mathMode.displayCramped then
return mathMode.textCramped
elseif mode == mathMode.text or mode == mathMode.textCramped then
return mathMode.scriptCramped
else return mathMode.scriptScriptCramped end
end
local function getRightMostGlyphId(node)
while node and node:is_a(elements.stackbox) and node.direction == 'H' do
node = node.children[#(node.children)]
end
if node and node:is_a(elements.text) then
return node.value.glyphString[#(node.value.glyphString)]
else
return 0
end
end
local function maxLength(...)
local arg = {...}
local m
for i, v in ipairs(arg) do
if i == 1 then
m = v
else
if v.length:tonumber() > m.length:tonumber() then
m = v
end
end
end
return m
end
local function scaleWidth(length, line)
local number = length.length
if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
number = number + length.shrink * line.ratio
elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
number = number + length.stretch * line.ratio
end
return number
end
elements.mbox = pl.class(nodefactory.hbox)
elements.mbox._type = "Mbox"
function elements.mbox:__tostring ()
return self._type
end
function elements.mbox:_init ()
nodefactory.hbox._init(self)
self.font = {}
self.children = {} self.relX = SILE.length(0) self.relY = SILE.length(0) self.value = {}
self.mode = mathMode.display
self.atom = atomType.ordinary
local font = {
family = SILE.settings:get("math.font.family"),
size = SILE.settings:get("math.font.size"),
style = SILE.settings:get("math.font.style"),
weight = SILE.settings:get("math.font.weight")
}
local filename = SILE.settings:get("math.font.filename")
if filename and filename ~= "" then font.filename = filename end
self.font = SILE.font.loadDefaults(font)
end
function elements.mbox.styleChildren (_)
SU.error("styleChildren is a virtual function that need to be overriden by its child classes")
end
function elements.mbox.shape (_, _, _)
SU.error("shape is a virtual function that need to be overriden by its child classes")
end
function elements.mbox.output (_, _, _, _)
SU.error("output is a virtual function that need to be overriden by its child classes")
end
function elements.mbox:getMathMetrics ()
return retrieveMathTable(self.font)
end
function elements.mbox:getScaleDown ()
local constants = self:getMathMetrics().constants
local scaleDown
if isScriptMode(self.mode) then
scaleDown = constants.scriptPercentScaleDown
elseif isScriptScriptMode(self.mode) then
scaleDown = constants.scriptScriptPercentScaleDown
else
scaleDown = 1
end
return scaleDown
end
function elements.mbox:styleDescendants ()
self:styleChildren()
for _, n in ipairs(self.children) do
if n then n:styleDescendants() end
end
end
function elements.mbox:shapeTree ()
for _, n in ipairs(self.children) do
if n then n:shapeTree() end
end
self:shape()
end
function elements.mbox:outputTree (x, y, line)
self:output(x, y, line)
local debug = SILE.settings:get("math.debug.boxes")
if debug and not (self:is_a(elements.space)) then
SILE.outputter:setCursor(scaleWidth(x, line), y.length)
SILE.outputter:debugHbox(
{ height = self.height.length,
depth = self.depth.length },
scaleWidth(self.width, line)
)
end
for _, n in ipairs(self.children) do
if n then n:outputTree(x + n.relX, y + n.relY, line) end
end
end
local spaceKind = {
thin = "thin",
med = "med",
thick = "thick",
}
local spacingRules = {
[atomType.ordinary] = {
[atomType.bigOperator] = {spaceKind.thin},
[atomType.binaryOperator] = {spaceKind.med, notScript = true},
[atomType.relationalOperator] = {spaceKind.thick, notScript = true},
[atomType.inner] = {spaceKind.thin, notScript = true}
},
[atomType.bigOperator] = {
[atomType.ordinary] = {spaceKind.thin},
[atomType.bigOperator] = {spaceKind.thin},
[atomType.relationalOperator] = {spaceKind.thick, notScript = true},
[atomType.inner] = {spaceKind.thin, notScript = true},
},
[atomType.binaryOperator] = {
[atomType.ordinary] = {spaceKind.med, notScript = true},
[atomType.bigOperator] = {spaceKind.med, notScript = true},
[atomType.openingSymbol] = {spaceKind.med, notScript = true},
[atomType.inner] = {spaceKind.med, notScript = true}
},
[atomType.relationalOperator] = {
[atomType.ordinary] = {spaceKind.thick, notScript = true},
[atomType.bigOperator] = {spaceKind.thick, notScript = true},
[atomType.openingSymbol] = {spaceKind.thick, notScript = true},
[atomType.inner] = {spaceKind.thick, notScript = true}
},
[atomType.closeSymbol] = {
[atomType.bigOperator] = {spaceKind.thin},
[atomType.binaryOperator] = {spaceKind.med, notScript = true},
[atomType.relationalOperator] = {spaceKind.thick, notScript = true},
[atomType.inner] = {spaceKind.thin, notScript = true}
},
[atomType.punctuationSymbol] = {
[atomType.ordinary] = {spaceKind.thin, notScript = true},
[atomType.bigOperator] = {spaceKind.thin, notScript = true},
[atomType.relationalOperator] = {spaceKind.thin, notScript = true},
[atomType.openingSymbol] = {spaceKind.thin, notScript = true},
[atomType.closeSymbol] = {spaceKind.thin, notScript = true},
[atomType.punctuationSymbol] = {spaceKind.thin, notScript = true},
[atomType.inner] = {spaceKind.thin, notScript = true}
},
[atomType.inner] = {
[atomType.ordinary] = {spaceKind.thin, notScript = true},
[atomType.bigOperator] = {spaceKind.thin},
[atomType.binaryOperator] = {spaceKind.med, notScript = true},
[atomType.relationalOperator] = {spaceKind.thick, notScript = true},
[atomType.openingSymbol] = {spaceKind.thin, notScript = true},
[atomType.punctuationSymbol] = {spaceKind.thin, notScript = true},
[atomType.inner] = {spaceKind.thin, notScript = true}
}
}
elements.stackbox = pl.class(elements.mbox)
elements.stackbox._type = "Stackbox"
function elements.stackbox:__tostring ()
local result = self.direction.."Box("
for i, n in ipairs(self.children) do
result = result..(i == 1 and "" or ", ")..tostring(n)
end
result = result..")"
return result
end
function elements.stackbox:_init (direction, children)
elements.mbox._init(self)
if not (direction == "H" or direction == "V") then
SU.error("Wrong direction '"..direction.."'; should be H or V")
end
self.direction = direction
self.children = children
end
function elements.stackbox:styleChildren ()
for _, n in ipairs(self.children) do
n.mode = self.mode
end
if self.direction == "H" then
local spaces = {}
for i = 1, #self.children-1 do
local v = self.children[i]
local v2 = self.children[i + 1]
if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
local rule = spacingRules[v.atom][v2.atom]
if not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
spaces[i+1] = rule[1]
end
end
end
local spaceIdx = {}
for i, _ in pairs(spaces) do
table.insert(spaceIdx, i)
end
table.sort(spaceIdx, function(a, b) return a > b end)
for _, idx in ipairs(spaceIdx) do
local hsp = elements.space(spaces[idx], 0, 0)
table.insert(self.children, idx, hsp)
end
end
end
function elements.stackbox:shape ()
self.height = SILE.length(0)
self.depth = SILE.length(0)
if self.direction == "H" then
for i, n in ipairs(self.children) do
n.relY = SILE.length(0)
self.height = i == 1 and n.height or maxLength(self.height, n.height)
self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
end
for _, elt in ipairs(self.children) do
if elt.is_a(elements.text) and elt.kind == 'operator'
and elt.stretchy then
elt:stretchyReshape(self.depth, self.height)
end
end
self.width = SILE.length(0)
for i, n in ipairs(self.children) do
n.relX = self.width
self.width = i == 1 and n.width or self.width + n.width
end
else for i, n in ipairs(self.children) do
n.relX = SILE.length(0)
self.width = i == 1 and n.width or maxLength(self.width, n.width)
end
for i, n in ipairs(self.children) do
self.depth = i == 1 and n.depth or self.depth + n.depth
end
for i = 1, #self.children do
local n = self.children[i]
if i == 1 then
self.height = n.height
self.depth = n.depth
elseif i > 1 then
n.relY = self.children[i - 1].relY + self.children[i - 1].depth + n.height
self.depth = self.depth + n.height + n.depth
end
end
end
end
function elements.stackbox:outputYourself (typesetter, line)
local mathX = typesetter.frame.state.cursorX
local mathY = typesetter.frame.state.cursorY
self:outputTree(self.relX + mathX, self.relY + mathY, line)
typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
end
function elements.stackbox.output (_, _, _, _) end
elements.subscript = pl.class(elements.mbox)
elements.subscript._type = "Subscript"
function elements.subscript:__tostring ()
return (self.sub and "Subscript" or "Superscript") .. "(" ..
tostring(self.base) .. ", " .. tostring(self.sub or self.super) .. ")"
end
function elements.subscript:_init (base, sub, sup)
elements.mbox._init(self)
self.base = base
self.sub = sub
self.sup = sup
if self.base then table.insert(self.children, self.base) end
if self.sub then table.insert(self.children, self.sub) end
if self.sup then table.insert(self.children, self.sup) end
self.atom = self.base.atom
end
function elements.subscript:styleChildren ()
if self.base then self.base.mode = self.mode end
if self.sub then self.sub.mode = getSubscriptMode(self.mode) end
if self.sup then self.sup.mode = getSuperscriptMode(self.mode) end
end
function elements.subscript:calculateItalicsCorrection ()
local lastGid = getRightMostGlyphId(self.base)
if lastGid > 0 then
local mathMetrics = self:getMathMetrics()
if mathMetrics.italicsCorrection[lastGid] then
return mathMetrics.italicsCorrection[lastGid]
end
end
return 0
end
function elements.subscript:shape ()
local mathMetrics = self:getMathMetrics()
local constants = mathMetrics.constants
local scaleDown = self:getScaleDown()
if self.base then
self.base.relX = SILE.length(0)
self.base.relY = SILE.length(0)
self.width = self.base.widthForSubscript or self.base.width
else
self.width = SILE.length(0)
end
local itCorr = self:calculateItalicsCorrection() * scaleDown
local subShift
local supShift
if self.sub then
if self.isUnderOver or self.base.largeop then
subShift = -itCorr
else
subShift = 0
end
self.sub.relX = self.width + subShift
self.sub.relY = SILE.length(math.max(
constants.subscriptShiftDown * scaleDown,
(self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
))
if (self:is_a(elements.underOver)
or self:is_a(elements.stackbox) or self.base.largeop) then
self.sub.relY = maxLength(self.sub.relY,
self.base.depth + constants.subscriptBaselineDropMin*scaleDown)
end
end
if self.sup then
if self.isUnderOver or self.base.largeop then
supShift = 0
else
supShift = itCorr
end
self.sup.relX = self.width + supShift
self.sup.relY = SILE.length(math.max(
isCrampedMode(self.mode)
and constants.superscriptShiftUpCramped * scaleDown
or constants.superscriptShiftUp * scaleDown, (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
)) * (-1)
if (self:is_a(elements.underOver)
or self:is_a(elements.stackbox) or self.base.largeop) then
self.sup.relY = maxLength(
(0-self.sup.relY),
self.base.height - constants.superscriptBaselineDropMax
* scaleDown) * (-1)
end
end
if self.sub and self.sup then
local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
self.sub.relY = constants.subSuperscriptGapMin * scaleDown
+ self.sub.height + self.sup.relY + self.sup.depth
local psi = constants.superscriptBottomMaxWithSubscript*scaleDown
+ self.sup.relY + self.sup.depth
if psi:tonumber() > 0 then
self.sup.relY = self.sup.relY - psi
self.sub.relY = self.sub.relY - psi
end
end
end
self.width = self.width + maxLength(
self.sub and self.sub.width + subShift or SILE.length(0),
self.sup and self.sup.width + supShift or SILE.length(0)
) + constants.spaceAfterScript * scaleDown
self.height = maxLength(
self.base and self.base.height or SILE.length(0),
self.sub and (self.sub.height - self.sub.relY) or SILE.length(0),
self.sup and (self.sup.height - self.sup.relY) or SILE.length(0)
)
self.depth = maxLength(
self.base and self.base.depth or SILE.length(0),
self.sub and (self.sub.depth + self.sub.relY) or SILE.length(0),
self.sup and (self.sup.depth + self.sup.relY) or SILE.length(0)
)
end
function elements.subscript.output (_, _, _, _) end
elements.underOver = pl.class(elements.subscript)
elements.underOver._type = "UnderOver"
function elements.underOver:__tostring ()
return self._type .. "(" .. tostring(self.base) .. ", " ..
tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
end
function elements.underOver:_init (base, sub, sup)
elements.mbox._init(self)
self.atom = base.atom
self.base = base
self.sub = sub
self.sup = sup
if self.sup then table.insert(self.children, self.sup) end
if self.base then
table.insert(self.children, self.base)
end
if self.sub then table.insert(self.children, self.sub) end
end
function elements.underOver:styleChildren ()
if self.base then self.base.mode = self.mode end
if self.sub then self.sub.mode = getSubscriptMode(self.mode) end
if self.sup then self.sup.mode = getSuperscriptMode(self.mode) end
end
function elements.underOver:shape ()
if not (self.mode == mathMode.display
or self.mode == mathMode.displayCramped) then
self.isUnderOver = true
elements.subscript.shape(self)
return
end
local constants = self:getMathMetrics().constants
local scaleDown = self:getScaleDown()
if self.base then
self.base.relY = SILE.length(0)
end
if self.sub then
self.sub.relY = self.base.depth + SILE.length(math.max(
(self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
constants.lowerLimitBaselineDropMin * scaleDown))
end
if self.sup then
self.sup.relY = 0 - self.base.height - SILE.length(math.max(
(constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
constants.upperLimitBaselineRiseMin * scaleDown))
end
local widest, a, b
if self.sub and self.sub.width > self.base.width then
if self.sup and self.sub.width > self.sup.width then
widest = self.sub
a = self.base
b = self.sup
elseif self.sup then
widest = self.sup
a = self.base
b = self.sub
else
widest = self.sub
a = self.base
b = nil
end
else
if self.sup and self.base.width > self.sup.width then
widest = self.base
a = self.sub
b = self.sup
elseif self.sup then
widest = self.sup
a = self.base
b = self.sub
else
widest = self.base
a = self.sub
b = nil
end
end
widest.relX = SILE.length(0)
local c = widest.width / 2
if a then a.relX = c - a.width / 2 end
if b then b.relX = c - b.width / 2 end
local itCorr = self:calculateItalicsCorrection() * scaleDown
if self.sup then self.sup.relX = self.sup.relX + itCorr / 2 end
if self.sub then self.sub.relX = self.sub.relX - itCorr / 2 end
self.width = maxLength(
self.base and self.base.width or SILE.length(0),
self.sub and self.sub.width or SILE.length(0),
self.sup and self.sup.width or SILE.length(0)
)
if self.sup then
self.height = 0 - self.sup.relY + self.sup.height
else
self.height = self.base and self.base.height or 0
end
if self.sub then
self.depth = self.sub.relY + self.sub.depth
else
self.depth = self.base and self.base.depth or 0
end
end
function elements.underOver:calculateItalicsCorrection ()
local lastGid = getRightMostGlyphId(self.base)
if lastGid > 0 then
local mathMetrics = self:getMathMetrics()
if mathMetrics.italicsCorrection[lastGid] then
local c = mathMetrics.italicsCorrection[lastGid]
if self.base.atom == atomType.bigOperator and isDisplayMode(self.mode) then
c = c * (self.base and self.base.font.size / self.font.size or 1.0)
end
return c
end
end
return 0
end
function elements.underOver.output (_, _, _, _) end
elements.terminal = pl.class(elements.mbox)
elements.terminal._type = "Terminal"
function elements.terminal:_init ()
elements.mbox._init(self)
end
function elements.terminal.styleChildren (_) end
function elements.terminal.shape (_) end
elements.space = pl.class(elements.terminal)
elements.space._type = "Space"
function elements.space:_init ()
elements.terminal._init(self)
end
function elements.space:__tostring ()
return self._type .. "(width=" .. tostring(self.width) ..
", height=" .. tostring(self.height) ..
", depth=" .. tostring(self.depth) .. ")"
end
local function getStandardLength (value)
if type(value) == "string" then
local direction = 1
if value:sub(1, 1) == "-" then
value = value:sub(2, -1)
direction = -1
end
if value == "thin" then
return SILE.length("3mu") * direction
elseif value == "med" then
return SILE.length("4mu plus 2mu minus 4mu") * direction
elseif value == "thick" then
return SILE.length("5mu plus 5mu") * direction
end
end
return SILE.length(value)
end
function elements.space:_init (width, height, depth)
elements.terminal._init(self)
self.width = getStandardLength(width)
self.height = getStandardLength(height)
self.depth = getStandardLength(depth)
end
function elements.space:shape ()
self.width = self.width:absolute() * self:getScaleDown()
self.height = self.height:absolute() * self:getScaleDown()
self.depth = self.depth:absolute() * self:getScaleDown()
end
function elements.space.output (_) end
elements.text = pl.class(elements.terminal)
elements.text._type = "Text"
function elements.text:__tostring ()
return self._type .. "(atom=" .. tostring(self.atom) ..
", kind=" .. tostring(self.kind) ..
", script=" .. tostring(self.script) ..
(self.stretchy and ", stretchy" or "") ..
(self.largeop and ", largeop" or "") ..
", text=\"" .. (self.originalText or self.text) .. "\")"
end
function elements.text:_init (kind, attributes, script, text)
elements.terminal._init(self)
if not (kind == "number" or kind == "identifier" or kind == "operator") then
SU.error("Unknown text node kind '"..kind.."'; should be one of: number, identifier, operator.")
end
self.kind = kind
self.script = script
self.text = text
if self.script ~= 'upright' then
local converted = ""
for _, uchr in luautf8.codes(self.text) do
local dst_char = luautf8.char(uchr)
if uchr >= 0x41 and uchr <= 0x5A then dst_char = luautf8.char(mathScriptConversionTable.capital[self.script](uchr))
elseif uchr >= 0x61 and uchr <= 0x7A then dst_char = luautf8.char(mathScriptConversionTable.small[self.script](uchr))
end
converted = converted..dst_char
end
self.originalText = self.text
self.text = converted
end
if self.kind == 'operator' then
if self.text == "-" then
self.text = "−"
end
end
for attribute,value in pairs(attributes) do
self[attribute] = value
end
end
function elements.text:shape ()
self.font.size = self.font.size * self:getScaleDown()
local face = SILE.font.cache(self.font, SILE.shaper.getFace)
local mathMetrics = self:getMathMetrics()
local glyphs = SILE.shaper:shapeToken(self.text, self.font)
if isDisplayMode(self.mode) and self.largeop then
glyphs = pl.tablex.deepcopy(glyphs)
local constructions = mathMetrics.mathVariants
.vertGlyphConstructions[glyphs[1].gid]
if constructions then
local displayVariants = constructions.mathGlyphVariantRecord
local biggest
local m = 0
for _, v in ipairs(displayVariants) do
if v.advanceMeasurement > m then
biggest = v
m = v.advanceMeasurement
end
end
if biggest then
glyphs[1].gid = biggest.variantGlyph
local dimen = hb.get_glyph_dimensions(face,
self.font.size, biggest.variantGlyph)
glyphs[1].width = dimen.width
glyphs[1].glyphAdvance = dimen.glyphAdvance
local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
local y_size = dimen.height + dimen.depth
glyphs[1].height = y_size / 2 + axisHeight
glyphs[1].depth = y_size / 2 - axisHeight
glyphs[1].fontHeight = dimen.height
glyphs[1].fontDepth = dimen.depth
end
end
end
SILE.shaper:preAddNodes(glyphs, self.value)
self.value.items = glyphs
self.value.glyphString = {}
if glyphs and #glyphs > 0 then
for i = 1, #glyphs do
table.insert(self.value.glyphString, glyphs[i].gid)
end
self.width = SILE.length(0)
self.widthForSubscript = SILE.length(0)
for i = #glyphs, 1, -1 do
self.width = self.width + glyphs[i].glyphAdvance
end
self.widthForSubscript = self.width
local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
if itCorr then
self.width = self.width + itCorr * self:getScaleDown()
end
for i = 1, #glyphs do
self.height = i == 1 and SILE.length(glyphs[i].height) or SILE.length(math.max(self.height:tonumber(), glyphs[i].height))
self.depth = i == 1 and SILE.length(glyphs[i].depth) or SILE.length(math.max(self.depth:tonumber(), glyphs[i].depth))
end
else
self.width = SILE.length(0)
self.height = SILE.length(0)
self.depth = SILE.length(0)
end
end
function elements.text:stretchyReshape (depth, height)
local mathMetrics = self:getMathMetrics()
local upem = mathMetrics.unitsPerEm
local sz = self.font.size
local requiredAdvance = (depth + height):tonumber() * upem/sz
SU.debug("math", "stretch: rA =", requiredAdvance)
local glyphs = pl.tablex.deepcopy(self.value.items)
local constructions = self:getMathMetrics().mathVariants
.vertGlyphConstructions[glyphs[1].gid]
if constructions then
local variants = constructions.mathGlyphVariantRecord
SU.debug("math", "stretch: variants =", variants)
local closest
local closestI
local m = requiredAdvance - (self.depth+self.height):tonumber() * upem/sz
SU.debug("math", "strech: m =", m)
for i,v in ipairs(variants) do
local diff = math.abs(v.advanceMeasurement - requiredAdvance)
SU.debug("math", "stretch: diff =", diff)
if diff < m then
closest = v
closestI = i
m = diff
end
end
SU.debug("math", "stretch: closestI =", tostring(closestI))
if closest then
glyphs[1].gid = closest.variantGlyph
local face = SILE.font.cache(self.font, SILE.shaper.getFace)
local dimen = hb.get_glyph_dimensions(face,
self.font.size, closest.variantGlyph)
glyphs[1].width = dimen.width
glyphs[1].height = dimen.height
glyphs[1].depth = dimen.depth
glyphs[1].glyphAdvance = dimen.glyphAdvance
self.width = SILE.length(dimen.glyphAdvance)
self.depth = SILE.length(dimen.depth)
self.height = SILE.length(dimen.height)
SILE.shaper:preAddNodes(glyphs, self.value)
self.value.items = glyphs
self.value.glyphString = {glyphs[1].gid}
end
end
end
function elements.text:output (x, y, line)
if not self.value.glyphString then return end
local compensatedY
if isDisplayMode(self.mode)
and self.atom == atomType.bigOperator
and self.value.items[1].fontDepth then
compensatedY = SILE.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
else
compensatedY = y
end
SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
SILE.outputter:setFont(self.font)
local width = self.width.length
SILE.outputter:drawHbox(self.value, width)
end
elements.fraction = pl.class(elements.mbox)
elements.fraction._type = "Fraction"
function elements.fraction:__tostring ()
return self._type .. "(" .. tostring(self.numerator) .. ", " ..
tostring(self.denominator) .. ")"
end
function elements.fraction:_init (numerator, denominator)
elements.mbox._init(self)
self.numerator = numerator
self.denominator = denominator
table.insert(self.children, numerator)
table.insert(self.children, denominator)
end
function elements.fraction:styleChildren ()
self.numerator.mode = getNumeratorMode(self.mode)
self.denominator.mode = getDenominatorMode(self.mode)
end
function elements.fraction:shape ()
local widest, other
if self.denominator.width > self.numerator.width then
widest, other = self.denominator, self.numerator
else
widest, other = self.numerator, self.denominator
end
widest.relX = SILE.length(0)
other.relX = (widest.width - other.width) / 2
self.width = widest.width
local constants = self:getMathMetrics().constants
local scaleDown = self:getScaleDown()
self.axisHeight = constants.axisHeight * scaleDown
self.ruleThickness = constants.fractionRuleThickness * scaleDown
if isDisplayMode(self.mode) then
self.numerator.relY = -self.axisHeight - self.ruleThickness/2 - SILE.length(math.max(
(constants.fractionNumDisplayStyleGapMin*scaleDown + self.numerator.depth):tonumber(),
constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
- self.axisHeight - self.ruleThickness/2))
else
self.numerator.relY = -self.axisHeight - self.ruleThickness/2 - SILE.length(math.max(
(constants.fractionNumeratorGapMin*scaleDown + self.numerator.depth):tonumber(),
constants.fractionNumeratorShiftUp * scaleDown - self.axisHeight
- self.ruleThickness/2))
end
if isDisplayMode(self.mode) then
self.denominator.relY = -self.axisHeight + self.ruleThickness/2 + SILE.length(math.max(
(constants.fractionDenomDisplayStyleGapMin * scaleDown
+ self.denominator.height):tonumber(),
constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
+ self.axisHeight - self.ruleThickness/2))
else
self.denominator.relY = -self.axisHeight + self.ruleThickness/2 + SILE.length(math.max(
(constants.fractionDenominatorGapMin * scaleDown
+ self.denominator.height):tonumber(),
constants.fractionDenominatorShiftDown * scaleDown
+ self.axisHeight - self.ruleThickness/2))
end
self.height = self.numerator.height - self.numerator.relY
self.depth = self.denominator.relY + self.denominator.depth
end
function elements.fraction:output (x, y, line)
SILE.outputter:drawRule(
scaleWidth(x, line),
y.length - self.axisHeight - self.ruleThickness / 2,
scaleWidth(self.width, line), self.ruleThickness)
end
local function newSubscript (spec)
return elements.subscript(spec.base, spec.sub, spec.sup)
end
local function newUnderOver (spec)
return elements.underOver(spec.base, spec.sub, spec.sup)
end
local function mapList (f, l)
local ret = {}
for i, x in ipairs(l) do
ret[i] = f(i, x)
end
return ret
end
elements.mtr = pl.class(elements.mbox)
function elements.mtr:_init (children)
self.children = children
end
function elements.mtr:styleChildren ()
for _, c in ipairs(self.children) do
c.mode = self.mode
end
end
function elements.mtr.shape (_) end
function elements.mtr.output (_) end
elements.table = pl.class(elements.mbox)
elements.table._type = "table"
function elements.table:_init (children, options)
elements.mbox._init(self)
self.children = children
self.options = options
self.nrows = #self.children
self.ncols = math.max(table.unpack(mapList(function(_, row)
return #row.children end, self.children)))
SU.debug("math", "self.ncols =", self.ncols)
self.rowspacing = self.options.rowspacing and SILE.length(self.options.rowspacing)
or SILE.length("7pt")
self.columnspacing = self.options.columnspacing and SILE.length(self.options.columnspacing)
or SILE.length("6pt")
for i,row in ipairs(self.children) do
for j = 1, (self.ncols - #row.children) do
SU.debug("math", "padding i =", i, "j =", j)
table.insert(row.children, elements.stackbox('H', {}))
SU.debug("math", "size", #row.children)
end
end
if options.columnalign then
local l = {}
for w in string.gmatch(options.columnalign, "[^%s]+") do
if not (w == "left" or w == "center" or w == "right") then
SU.error("Invalid specifier in `columnalign` attribute: "..w)
end
table.insert(l, w)
end
for _ = 1, (self.ncols - #l), 1 do
table.insert(l, l[#l])
end
for _ = 1, (#l - self.ncols), 1 do
table.remove(l)
end
self.options.columnalign = l
else
self.options.columnalign = pl.List.range(1, self.ncols):map(function(_) return "center" end)
end
end
function elements.table:styleChildren ()
if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
for _,c in ipairs(self.children) do
c.mode = mathMode.display
end
else
for _,c in ipairs(self.children) do
c.mode = mathMode.text
end
end
end
function elements.table:shape ()
for _,row in ipairs(self.children) do
row.height = SILE.length(0)
row.depth = SILE.length(0)
for _,cell in ipairs(row.children) do
row.height = maxLength(row.height, cell.height)
row.depth = maxLength(row.depth, cell.depth)
end
end
self.vertSize = SILE.length(0)
for i, row in ipairs(self.children) do
self.vertSize = self.vertSize + row.height + row.depth +
(i == self.nrows and SILE.length(0) or self.rowspacing) end
local rowHeightSoFar = SILE.length(0)
for i, row in ipairs(self.children) do
row.relY = rowHeightSoFar + row.height - self.vertSize
rowHeightSoFar = rowHeightSoFar + row.height + row.depth +
(i == self.nrows and SILE.length(0) or self.rowspacing) end
self.width = SILE.length(0)
local thisColRelX = SILE.length(0)
for i = 1,self.ncols do
local columnWidth = SILE.length(0)
for j = 1,self.nrows do
if self.children[j].children[i].width > columnWidth then
columnWidth = self.children[j].children[i].width
end
end
for j = 1,self.nrows do
local cell = self.children[j].children[i]
if self.options.columnalign[i] == "left" then
cell.relX = thisColRelX
elseif self.options.columnalign[i] == "center" then
cell.relX = thisColRelX + (columnWidth - cell.width) / 2
elseif self.options.columnalign[i] == "right" then
cell.relX = thisColRelX + (columnWidth - cell.width)
else
SU.error("invalid columnalign parameter")
end
end
thisColRelX = thisColRelX + columnWidth +
(i == self.ncols and SILE.length(0) or self.columnspacing) end
self.width = thisColRelX
local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
self.height = self.vertSize / 2 + axisHeight
self.depth = self.vertSize / 2 - axisHeight
for _,row in ipairs(self.children) do
row.relY = row.relY + self.vertSize / 2 - axisHeight
row.width = self.width
end
end
function elements.table.output (_) end
elements.mathMode = mathMode
elements.atomType = atomType
elements.scriptType = scriptType
elements.mathVariantToScriptType = mathVariantToScriptType
elements.symbolDefaults = symbolDefaults
elements.newSubscript = newSubscript
elements.newUnderOver = newUnderOver
return elements