local nodefactory = require("types.node")
local hb = require("justenoughharfbuzz")
local ot = require("core.opentype-parser")
local syms = require("packages.math.unicode-symbols")
local mathvariants = require("packages.math.unicode-mathvariants")
local convertMathVariantScript = mathvariants.convertMathVariantScript
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 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 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 fontHasMathTable, rawMathTable, mathTableParsable, mathTable
fontHasMathTable, rawMathTable = pcall(hb.get_table, face, "MATH")
if fontHasMathTable then
mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
end
if not fontHasMathTable or not mathTableParsable then
SU.error(([[
You must use a math font for math rendering
The math table in '%s' could not be %s.
]]):format(face.filename, fontHasMathTable and "parsed" or "loaded"))
end
local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
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.types.length(0) self.relY = SILE.types.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 overridden by its child classes")
end
function elements.mbox.shape (_, _, _)
SU.error("shape is a virtual function that need to be overridden by its child classes")
end
function elements.mbox.output (_, _, _, _)
SU.error("output is a virtual function that need to be overridden 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.types.length(0)
self.depth = SILE.types.length(0)
if self.direction == "H" then
for i, n in ipairs(self.children) do
n.relY = SILE.types.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 SU.boolean(elt.stretchy, false) then
elt:_vertStretchyReshape(self.depth, self.height)
end
end
self.width = SILE.types.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.types.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.phantom = pl.class(elements.stackbox) elements.phantom._type = "Phantom"
function elements.phantom:_init (children)
elements.stackbox._init(self, "H", children)
end
function elements.phantom:output (_, _, _)
self.children = {}
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.types.length(0)
self.base.relY = SILE.types.length(0)
self.width = self.base.widthForSubscript or self.base.width
else
self.width = SILE.types.length(0)
end
local itCorr = self:calculateItalicsCorrection() * scaleDown
local isBaseSymbol = not self.base or self.base:is_a(elements.terminal)
local isBaseLargeOp = SU.boolean(self.base and self.base.largeop, false)
local subShift
local supShift
if self.sub then
if self.isUnderOver or isBaseLargeOp then
subShift = -itCorr
else
subShift = 0
end
self.sub.relX = self.width + subShift
self.sub.relY = SILE.types.length(
math.max(
constants.subscriptShiftDown * scaleDown,
isBaseSymbol and 0 or (self.base.depth + constants.subscriptBaselineDropMin * scaleDown):tonumber(),
(self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
)
)
if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or isBaseLargeOp then
self.sub.relY = maxLength(self.sub.relY, self.base.depth + constants.subscriptBaselineDropMin * scaleDown)
end
end
if self.sup then
if self.isUnderOver or isBaseLargeOp then
supShift = 0
else
supShift = itCorr
end
self.sup.relX = self.width + supShift
self.sup.relY = SILE.types.length(
math.max(
isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
or constants.superscriptShiftUp * scaleDown,
isBaseSymbol and 0 or (self.base.height - constants.superscriptBaselineDropMax * scaleDown):tonumber(),
(self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
)
) * -1
if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or isBaseLargeOp 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.types.length(0),
self.sup and self.sup.width + supShift or SILE.types.length(0)
)
+ constants.spaceAfterScript * scaleDown
self.height = maxLength(
self.base and self.base.height or SILE.types.length(0),
self.sub and (self.sub.height - self.sub.relY) or SILE.types.length(0),
self.sup and (self.sup.height - self.sup.relY) or SILE.types.length(0)
)
self.depth = maxLength(
self.base and self.base.depth or SILE.types.length(0),
self.sub and (self.sub.depth + self.sub.relY) or SILE.types.length(0),
self.sup and (self.sup.depth + self.sup.relY) or SILE.types.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
local function isNotEmpty (element)
return element and (element:is_a(elements.terminal) or #element.children > 0)
end
function elements.underOver:_init (base, sub, sup)
elements.mbox._init(self)
self.atom = base.atom
self.base = base
self.sub = isNotEmpty(sub) and sub or nil
self.sup = isNotEmpty(sup) and sup or nil
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:_stretchyReshapeToBase (part)
if #part.children == 0 then
local elt = part
if elt.is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
elt:_horizStretchyReshape(self.base.width)
end
elseif part:is_a(elements.underOver) then
local hasStretched = false
for _, elt in ipairs(part.children) do
if elt.is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
local stretched = elt:_horizStretchyReshape(self.base.width)
if stretched then
hasStretched = true
end
end
end
if hasStretched then
part:shape()
end
end
end
function elements.underOver:shape ()
local isMovableLimits = SU.boolean(self.base and self.base.movablelimits, false)
if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) and isMovableLimits 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.types.length(0)
end
if self.sub then
self:_stretchyReshapeToBase(self.sub)
self.sub.relY = self.base.depth
+ SILE.types.length(
math.max(
(self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
constants.lowerLimitBaselineDropMin * scaleDown
)
)
end
if self.sup then
self:_stretchyReshapeToBase(self.sup)
self.sup.relY = 0
- self.base.height
- SILE.types.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.types.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.types.length(0),
self.sub and self.sub.width or SILE.types.length(0),
self.sup and self.sup.width or SILE.types.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.types.length("3mu") * direction
elseif value == "med" then
return SILE.types.length("4mu plus 2mu minus 4mu") * direction
elseif value == "thick" then
return SILE.types.length("5mu plus 5mu") * direction
end
end
return SILE.types.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)
.. (SU.boolean(self.stretchy, false) and ", stretchy" or "")
.. (SU.boolean(self.largeop, false) 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" or kind == "string") then
SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
end
self.kind = kind
self.script = script
self.text = text
if self.script ~= "upright" then
local converted = convertMathVariantScript(self.text, self.script)
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 SU.boolean(self.largeop, false) 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.types.length(0)
self.widthForSubscript = SILE.types.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.types.length(glyphs[i].height)
or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
end
else
self.width = SILE.types.length(0)
self.height = SILE.types.length(0)
self.depth = SILE.types.length(0)
end
end
function elements.text.findClosestVariant (_, variants, requiredAdvance, currentAdvance)
local closest
local closestI
local m = requiredAdvance - currentAdvance
for i, variant in ipairs(variants) do
local diff = math.abs(variant.advanceMeasurement - requiredAdvance)
SU.debug("math", "stretch: diff =", diff)
if diff < m then
closest = variant
closestI = i
m = diff
end
end
return closest, closestI
end
function elements.text:_reshapeGlyph (glyph, closestVariant, sz)
local face = SILE.font.cache(self.font, SILE.shaper.getFace)
local dimen = hb.get_glyph_dimensions(face, sz, closestVariant.variantGlyph)
glyph.gid = closestVariant.variantGlyph
glyph.width, glyph.height, glyph.depth, glyph.glyphAdvance =
dimen.width, dimen.height, dimen.depth, dimen.glyphAdvance
return dimen
end
function elements.text:_stretchyReshape (target, direction)
local mathMetrics = self:getMathMetrics()
local upem = mathMetrics.unitsPerEm
local sz = self.font.size
local requiredAdvance = target:tonumber() * upem / sz
SU.debug("math", "stretch: rA =", requiredAdvance)
local glyphs = pl.tablex.deepcopy(self.value.items)
local glyphConstructions = direction and mathMetrics.mathVariants.vertGlyphConstructions
or mathMetrics.mathVariants.horizGlyphConstructions
local constructions = glyphConstructions[glyphs[1].gid]
if constructions then
local variants = constructions.mathGlyphVariantRecord
SU.debug("math", "stretch: variants =", variants)
local currentAdvance = (direction and (self.depth + self.height):tonumber() or self.width:tonumber()) * upem / sz
local closest, closestI = self:findClosestVariant(variants, requiredAdvance, currentAdvance)
SU.debug("math", "stretch: closestI =", closestI)
if closest then
local dimen = self:_reshapeGlyph(glyphs[1], closest, sz)
self.width, self.depth, self.height =
SILE.types.length(dimen.glyphAdvance), SILE.types.length(dimen.depth), SILE.types.length(dimen.height)
SILE.shaper:preAddNodes(glyphs, self.value)
self.value.items = glyphs
self.value.glyphString = { glyphs[1].gid }
return true
end
end
return false
end
function elements.text:_vertStretchyReshape (depth, height)
local hasStretched = self:_stretchyReshape(depth + height, true)
if hasStretched then
self.vertExpectedSz = height + depth
self.vertScalingRatio = (depth + height):tonumber() / (self.height:tonumber() + self.depth:tonumber())
self.height = height
self.depth = depth
end
return hasStretched
end
function elements.text:_horizStretchyReshape (width)
local hasStretched = self:_stretchyReshape(width, false)
if hasStretched then
self.horizScalingRatio = width:tonumber() / self.width:tonumber()
self.width = width
end
return hasStretched
end
function elements.text:output (x, y, line)
if not self.value.glyphString then
return
end
local compensatedY
if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) and self.value.items[1].fontDepth then
compensatedY = SILE.types.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
if SILE.outputter.scaleFn and (self.horizScalingRatio or self.vertScalingRatio) then
local xratio = self.horizScalingRatio or 1
local yratio = self.vertScalingRatio or 1
SU.debug("math", "fake glyph stretch: xratio =", xratio, "yratio =", yratio)
SILE.outputter:scaleFn(x, y, xratio, yratio, function ()
SILE.outputter:drawHbox(self.value, width)
end)
else
SILE.outputter:drawHbox(self.value, width)
end
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 (attributes, numerator, denominator)
elements.mbox._init(self)
self.numerator = numerator
self.denominator = denominator
self.attributes = attributes
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 ()
self.padding = SILE.types.length("1px"):absolute()
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 = self.padding
other.relX = self.padding + (widest.width - other.width) / 2
self.width = widest.width + 2 * self.padding
local constants = self:getMathMetrics().constants
local scaleDown = self:getScaleDown()
self.axisHeight = constants.axisHeight * scaleDown
self.ruleThickness = self.attributes.linethickness
and SU.cast("measurement", self.attributes.linethickness):tonumber()
or constants.fractionRuleThickness * scaleDown
local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
if isDisplayMode(self.mode) then
numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
else
numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
end
self.numerator.relY = -self.axisHeight
- self.ruleThickness / 2
- SILE.types.length(
math.max(
(numeratorGapMin + self.numerator.depth):tonumber(),
numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
)
)
self.denominator.relY = -self.axisHeight
+ self.ruleThickness / 2
+ SILE.types.length(
math.max(
(denominatorGapMin + self.denominator.height):tonumber(),
denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
)
)
self.height = self.numerator.height - self.numerator.relY
self.depth = self.denominator.relY + self.denominator.depth
end
function elements.fraction:output (x, y, line)
if self.ruleThickness > 0 then
SILE.outputter:drawRule(
scaleWidth(x + self.padding, line),
y.length - self.axisHeight - self.ruleThickness / 2,
scaleWidth(self.width - 2 * self.padding, line),
self.ruleThickness
)
end
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(pl.utils.unpack(mapList(function (_, row)
return #row.children
end, self.children)))
SU.debug("math", "self.ncols =", self.ncols)
local spacing = SILE.settings:get("math.font.size") * 0.6 self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or spacing
self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing) or spacing
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.types.length(0)
row.depth = SILE.types.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.types.length(0)
for i, row in ipairs(self.children) do
self.vertSize = self.vertSize
+ row.height
+ row.depth
+ (i == self.nrows and SILE.types.length(0) or self.rowspacing) end
local rowHeightSoFar = SILE.types.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.types.length(0) or self.rowspacing) end
self.width = SILE.types.length(0)
local thisColRelX = SILE.types.length(0)
for i = 1, self.ncols do
local columnWidth = SILE.types.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.types.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
local function getRadicandMode (mode)
return mode
end
local function getDegreeMode (mode)
if mode == mathMode.display then
return mathMode.scriptScript
elseif mode == mathMode.displayCramped then
return mathMode.scriptScriptCramped
elseif mode == mathMode.text or mode == mathMode.script or mode == mathMode.scriptScript then
return mathMode.scriptScript
end
return mathMode.scriptScriptCramped
end
elements.sqrt = pl.class(elements.mbox)
elements.sqrt._type = "Sqrt"
function elements.sqrt:__tostring ()
return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
end
function elements.sqrt:_init (radicand, degree)
elements.mbox._init(self)
self.radicand = radicand
if degree then
self.degree = degree
table.insert(self.children, degree)
end
table.insert(self.children, radicand)
self.relX = SILE.types.length()
self.relY = SILE.types.length()
end
function elements.sqrt:styleChildren ()
self.radicand.mode = getRadicandMode(self.mode)
if self.degree then
self.degree.mode = getDegreeMode(self.mode)
end
end
function elements.sqrt:shape ()
local mathMetrics = self:getMathMetrics()
local scaleDown = self:getScaleDown()
local constants = mathMetrics.constants
self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
else
self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
end
self.extraAscender = constants.radicalExtraAscender * scaleDown
local radicalGlyph = SILE.shaper:measureChar("√")
local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
/ (radicalGlyph.height + radicalGlyph.depth)
local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
self.offsetX = SILE.types.length()
if self.degree then
self.degree.relY = -constants.radicalDegreeBottomRaisePercent * self.symbolHeight
self.symbolShortHeight = self.symbolShortHeight - constants.radicalExtraAscender * scaleDown
self.offsetX = self.degree.width
+ constants.radicalKernBeforeDegree * scaleDown
+ constants.radicalKernAfterDegree * scaleDown
end
self.radicand.relX = self.symbolWidth + self.offsetX
self.width = self.radicand.width + self.symbolWidth + self.offsetX
self.height = self.symbolHeight + self.radicalVerticalGap + self.extraAscender
self.depth = self.radicand.depth
end
local function _r (number)
return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
end
function elements.sqrt:output (x, y, line)
local h = self.height:tonumber()
local d = self.depth:tonumber()
local s0 = scaleWidth(self.offsetX, line):tonumber()
local sw = scaleWidth(self.symbolWidth, line):tonumber()
local dsh = h - self.symbolShortHeight:tonumber()
local dsd = self.symbolDepth:tonumber()
local symbol = {
_r(self.radicalRuleThickness),
"w", 1,
"j", _r(sw + s0),
_r(self.extraAscender),
"m",
_r(s0 + sw * 0.90),
_r(self.extraAscender),
"l",
_r(s0 + sw * 0.4),
_r(h + d + dsd),
"l",
_r(s0 + sw * 0.2),
_r(dsh),
"l",
s0 + sw * 0.1,
_r(dsh + 0.5),
"l",
"S",
}
local svg = table.concat(symbol, " ")
local xscaled = scaleWidth(x, line)
SILE.outputter:drawSVG(svg, xscaled, y, sw, h, 1)
SILE.outputter:drawRule(
s0 + self.symbolWidth + xscaled,
y.length - self.height + self.extraAscender - self.radicalRuleThickness / 2,
scaleWidth(self.radicand.width, line),
self.radicalRuleThickness
)
end
elements.padded = pl.class(elements.mbox)
elements.padded._type = "Padded"
function elements.padded:__tostring ()
return self._type .. "(" .. tostring(self.impadded) .. ")"
end
function elements.padded:_init (attributes, impadded)
elements.mbox._init(self)
self.impadded = impadded
self.attributes = attributes or {}
table.insert(self.children, impadded)
end
function elements.padded:styleChildren ()
self.impadded.mode = self.mode
end
function elements.padded:shape ()
local width = self.attributes.width and SU.cast("measurement", self.attributes.width)
local height = self.attributes.height and SU.cast("measurement", self.attributes.height)
local depth = self.attributes.depth and SU.cast("measurement", self.attributes.depth)
local lspace = self.attributes.lspace and SU.cast("measurement", self.attributes.lspace)
local voffset = self.attributes.voffset and SU.cast("measurement", self.attributes.voffset)
width = width and (width:tonumber() > 0 and width or SILE.types.measurement())
height = height and (height:tonumber() > 0 and height or SILE.types.measurement())
depth = depth and (depth:tonumber() > 0 and depth or SILE.types.measurement())
lspace = lspace and (lspace:tonumber() > 0 and lspace or SILE.types.measurement())
voffset = voffset or SILE.types.measurement(0)
self.width = width and SILE.types.length(width) or self.impadded.width
self.height = height and SILE.types.length(height) or self.impadded.height
self.depth = depth and SILE.types.length(depth) or self.impadded.depth
self.impadded.relX = lspace and SILE.types.length(lspace) or SILE.types.length()
self.impadded.relY = voffset and SILE.types.length(voffset):negate() or SILE.types.length()
end
function elements.padded.output (_, _, _, _) end
elements.bevelledFraction = pl.class(elements.fraction) elements.fraction._type = "BevelledFraction"
function elements.bevelledFraction:shape ()
local constants = self:getMathMetrics().constants
local scaleDown = self:getScaleDown()
local hSkew = constants.skewedFractionHorizontalGap * scaleDown
local vSkewUp = isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
or constants.superscriptShiftUp * scaleDown
local vSkewDown = 0
self.ruleThickness = self.attributes.linethickness
and SU.cast("measurement", self.attributes.linethickness):tonumber()
or constants.fractionRuleThickness * scaleDown
self.numerator.relX = SILE.types.length(0)
self.numerator.relY = SILE.types.length(-vSkewUp)
self.denominator.relX = self.numerator.width + hSkew
self.denominator.relY = SILE.types.length(vSkewDown)
self.width = self.numerator.width + self.denominator.width + hSkew
self.height = maxLength(self.numerator.height + vSkewUp, self.denominator.height - vSkewDown)
self.depth = maxLength(self.numerator.depth - vSkewUp, self.denominator.depth + vSkewDown)
self.barWidth = SILE.types.length(hSkew)
self.barX = self.numerator.relX + self.numerator.width
end
function elements.bevelledFraction:output (x, y, line)
local h = self.height:tonumber()
local d = self.depth:tonumber()
local barwidth = scaleWidth(self.barWidth, line):tonumber()
local xscaled = scaleWidth(x + self.barX, line)
local rd = self.ruleThickness / 2
local symbol = {
_r(self.ruleThickness),
"w", 1,
"J", _r(0),
_r(d + h - rd),
"m",
_r(barwidth),
_r(rd),
"l",
"S",
}
local svg = table.concat(symbol, " ")
SILE.outputter:drawSVG(svg, xscaled, y, barwidth, h, 1)
end
elements.mathMode = mathMode
elements.atomType = atomType
elements.symbolDefaults = symbolDefaults
elements.newSubscript = newSubscript
elements.newUnderOver = newUnderOver
return elements