/
init.lua
198 lines (173 loc) · 10.4 KB
/
init.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
local base = require("packages.base")
local package = pl.class(base)
package._name = "dropcaps"
function package:_init ()
base._init(self)
self:loadPackage("rebox")
self:loadPackage("raiselower")
self:loadPackage("rules")
end
function package.declareSettings (_)
SILE.settings:declare({
parameter = "dropcaps.bsratio",
type = "number or nil",
default = nil,
help = "Default ratio of the descender to the baseline (around 0.3 in usual fonts)."
})
end
local metrics = require("fontmetrics")
local metricscache = {}
local getMetrics = function (l)
local fontoptions = SILE.font.loadDefaults({})
local m = metricscache[SILE.font._key(fontoptions)]
if not m then
local face = SILE.font.cache(fontoptions, SILE.shaper.getFace)
m = metrics.get_typographic_extents(face)
m.ascender = m.ascender
m.descender = m.descender
metricscache[SILE.font._key(fontoptions)] = m
end
return m
end
local function getToleranceDepth ()
-- In non-strict mode, we allow using more lines to fit the dropcap.
-- However we cannot just check if the "extra depth" of the dropcap is above 0.
-- - our depth adjustment is but a best attempt.
-- - Some characters may have a small depth of their own, such as the "O" in Gentium Plus.
-- We must just ensure they stay within reasonable bounds with respect to the baseline,
-- so as not to flow over the next lines.
-- We compute a tolerance ratio based on the font metrics.
-- If the doesn't work, the user can still set the dropcaps.bsratio setting.
-- With well-formed text font, a good compromise is around 0.3 (LaTeX does this too for defining its \strut).
if SILE.settings:get("dropcaps.bsratio") then
return SILE.settings.get("dropcaps.bsratio") * SILE.measurement("1bs"):tonumber()
end
local m = getMetrics()
local descenderRatio = m.descender / (m.ascender + m.descender)
return descenderRatio * SILE.measurement("1bs"):tonumber()
end
local shapeHbox = function (options, content)
-- Clear irrelevant values before passing to font
options.lines, options.join, options.raise, options.shift, options.color, options.scale = nil, nil, nil, nil, nil, nil
SILE.call("noindent")
local hbox = SILE.typesetter:makeHbox(function ()
SILE.call("font", options, content)
end)
return hbox
end
function package:registerCommands ()
-- This implementation relies on the hangafter and hangindent features of Knuth's line-breaking algorithm.
-- These features in core line breaking algorithm supply the blank space in the paragraph shape but don't fill it with anything.
self:registerCommand("dropcap", function (options, content)
local lines = SU.cast("integer", options.lines or 3)
local join = SU.boolean(options.join, false)
local standoff = SU.cast("measurement", options.standoff or "1spc")
local raise = SU.cast("measurement", options.raise or 0)
local shift = SU.cast("measurement", options.shift or 0)
local size = SU.cast("measurement or nil", options.size or nil)
local scale = SU.cast("number", options.scale or 1.0)
local strict = SU.boolean(options.strict, true)
local depthadjust = SU.boolean(options.depthadjust, false)
local color = options.color
options.size = nil -- we need to measure the "would have been" size before using this
if color then self:loadPackage("color") end
-- Some initial capital fonts have all their glyphs hanging below the baseline (e.g. EB Garamond Initials)
-- We cannot manage all pathological cases.
-- Quite empirically, we can shape an "I", which shouldn't usually have a depth normally.
-- If it has, then likely all glyphs do also and we need to compensate for that everywhere.
local depthAdjustment = depthadjust and shapeHbox(options, { "I" }).depth:tonumber() or 0
SU.debug("dropcaps", "Depth adjustment", depthAdjustment)
-- We want the drop cap to span over N lines, that is N - 1 baselineskip + the height of the first line.
-- Note this only works for the default linespace mechanism.
-- We determine the height of the first line by measuring the size the initial content *would have* been.
local tmpHbox = shapeHbox(options, content)
local extraHeight = SILE.measurement((lines - 1).."bs"):tonumber()
local curHeight = tmpHbox.height:tonumber() + depthAdjustment
local targetHeight = (curHeight - depthAdjustment) * scale + extraHeight
if strict then
-- Take into account the compensated depth of the initial
curHeight = curHeight + tmpHbox.depth:tonumber()
end
SU.debug("dropcaps", "Target height", targetHeight)
-- Now we need to figure out how to scale the dropcap font to get an initial of targetHeight.
-- From that we can also figure out the width it will be at that height.
local curSize = SILE.measurement(SILE.settings:get("font.size")):tonumber()
local curWidth = tmpHbox.width:tonumber()
options.size = size and size:tonumber() or (targetHeight / curHeight * curSize) --+ tmpHbox.depth:tonumber() / curSize
local targetWidth = curWidth / curSize * options.size
SU.debug("dropcaps", "Target font size", options.size)
SU.debug("dropcaps", "Target width", targetWidth)
-- Typeset the dropcap with its final shape, but don't output it yet.
local hbox = shapeHbox(options, content)
if not strict then
-- Compensation for regular extra depth.
local compensationHeight = depthAdjustment * options.size / curSize
SU.debug("dropcaps", "Compensation height", compensationHeight)
-- Some fonts have descenders on letters such as Q, J, etc.
-- In that case we may need extra lines to the dropcap.
local extraDepth = hbox.depth:tonumber() - compensationHeight
local toleranceDepth = getToleranceDepth()
if extraDepth > toleranceDepth then
SU.debug("dropcaps", "Extra depth", extraDepth, "> tolerance", toleranceDepth)
local extraLines = math.ceil((extraDepth - toleranceDepth) / SILE.measurement("1bs"):tonumber())
lines = lines + extraLines
SU.debug("dropcaps", "Extra lines needed to fit", extraLines)
else
SU.debug("dropcaps", "Extra depth", extraDepth, "< tolerance", toleranceDepth)
end
raise = raise:tonumber() + compensationHeight
else
raise = raise:tonumber() + hbox.depth:tonumber()
end
-- Setup up the necessary indents for the final paragraph content
local joinOffset = join and standoff:tonumber() or 0
SILE.settings:set("current.hangAfter", -lines)
SILE.settings:set("current.hangIndent", targetWidth + joinOffset)
SU.debug("dropcaps", "joinOffset", joinOffset)
-- The paragraph is indented so as to leave enough space for the drop cap.
-- We "trick" the typesetter with a zero-dimension box wrapping our original box.
SILE.call("rebox", { height = 0, depth = 0, width = -joinOffset }, function ()
SILE.call("glue", { width = shift - targetWidth - joinOffset })
SILE.call("lower", { height = extraHeight - raise }, function ()
SILE.call(color and "color" or "noop", { color = color }, function ()
SILE.typesetter:pushHbox(hbox)
end)
end)
end)
end, "Show an 'initial capital' (also known as a 'drop cap') at the start of the content paragraph.")
end
package.documentation = [[
\begin{document}
The \autodoc:package{dropcaps} package allows you to format paragraphs with an “initial capital” (also commonly referred as a “drop cap”), typically one large capital letter used as a decorative element at the beginning of a paragraph.
It provides the \autodoc:command{\dropcap} command.
The content passed will be the initial character(s).
The primary option is \autodoc:parameter{lines}, an integer specifying the number of lines to span (defaults to \code{3}).
The scale of the characters can be adjusted relative to the first line using the \autodoc:parameter{scale} option (defaults to \code{1.0}).
The \autodoc:parameter{join} parameter is a boolean for whether to join the dropcap to the first line (defaults to \code{false}).
If \autodoc:parameter{join} is \code{true}, the value of the \autodoc:parameter{standoff} option (defaults to \code{1spc}) is applied to all but the first line.
Optionally \autodoc:parameter{color} can be passed to change the typeface color, which is sometimes useful to offset the apparent weight of a large glyph.
To tweak the position of the dropcap, measurements may be passed to the \autodoc:parameter{raise} and \autodoc:parameter{shift} options.
Other options passed to \autodoc:command{\dropcap} will be passed through to \autodoc:command{\font} when drawing the initial letter(s).
This may be useful for passing OpenType options or other font preferences.
Some fonts have capitals — such as, typically, Q and J — hanging below the baseline.
By default, the dropcap fits the specified number of line and the characters are typeset in a smaller size to fit these descenders.
With the \autodoc:parameter{strict} option set to \code{false}, the characters are scaled with respect to their height only, and extra hanged lines are added to the dropcap to accommodate the descenders.
In that case, some fonts have capitals very slightly hanging below the baseline.
The dropcap is allowed to overflow the baseline by a reasonable amount, before triggering the addition of extra lines.
This tolerance is computed based on the font metrics.
If you want to bypass this mechanism and adjust the tolerance, you can use the \autodoc:setting{dropcaps.bsratio} setting.
Some fonts, such as EB Garamond Initials, have \em{all} capitals hanging below the baseline.
For the latter, you should let the \autodoc:parameter{strict} option to its default (\code{true}).
The \autodoc:parameter{depthadjust=true} option empirically adjusts the dropcap depth based of that of the I character.
Combined with \autodoc:parameter{strict=false}, it will work for EB Garamond Initials too.
While it is not possible to handle all pathological cases, these two options may help in some cases.
\begin{autodoc:note}
One caveat is that the size of the initials is calculated using the default linespacing mechanism.
If you are using an alternative method from the \autodoc:package{linespacing} package, you might see strange results.
Set the \autodoc:setting{document.baselineskip} to approximate your effective leading value for best results.
If that doesn't work set the size manually.
Using \code{SILE.setCommandDefaults()} can be helpful for so you don't have to set the size every time.
\end{autodoc:note}
\end{document}
]]
return package