• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sile-typesetter / sile / 9304049654

30 May 2024 02:12PM UTC coverage: 60.021% (-14.7%) from 74.707%
9304049654

push

github

web-flow
Merge 1a26b4f22 into a1fd105f8

6743 of 12900 new or added lines in 186 files covered. (52.27%)

347 existing lines in 49 files now uncovered.

10311 of 17179 relevant lines covered (60.02%)

3307.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/packages/dropcaps/init.lua
UNCOV
1
local base = require("packages.base")
×
2

UNCOV
3
local package = pl.class(base)
×
UNCOV
4
package._name = "dropcaps"
×
5

UNCOV
6
function package:_init ()
×
NEW
7
   base._init(self)
×
NEW
8
   self:loadPackage("rebox")
×
NEW
9
   self:loadPackage("raiselower")
×
10
end
11

UNCOV
12
function package.declareSettings (_)
×
NEW
13
   SILE.settings:declare({
×
14
      parameter = "dropcaps.bsratio",
15
      type = "number or nil",
16
      default = nil, -- nil means "use computed value based on font metrics"
17
      help = "When set, fixed default ratio of the descender with respect to the baseline (around 0.3 in usual fonts).",
18
   })
19
end
20

21
local function shapeHbox (options, content)
NEW
22
   local hbox = SILE.typesetter:makeHbox(function ()
×
NEW
23
      SILE.call("font", options, content)
×
24
   end)
NEW
25
   return hbox
×
26
end
27

UNCOV
28
local metrics = require("fontmetrics")
×
UNCOV
29
local bsratiocache = {}
×
30

31
local computeBaselineRatio = function ()
NEW
32
   local fontoptions = SILE.font.loadDefaults({})
×
NEW
33
   local bsratio = bsratiocache[SILE.font._key(fontoptions)]
×
NEW
34
   if not bsratio then
×
NEW
35
      local face = SILE.font.cache(fontoptions, SILE.shaper.getFace)
×
NEW
36
      local m = metrics.get_typographic_extents(face)
×
NEW
37
      bsratio = m.descender / (m.ascender + m.descender)
×
NEW
38
      bsratiocache[SILE.font._key(fontoptions)] = bsratio
×
39
   end
NEW
40
   return bsratio
×
41
end
42

43
local function getToleranceDepth ()
44
   -- In non-strict mode, we allow using more lines to fit the dropcap.
45
   -- However we cannot just check if the "extra depth" of the dropcap is above 0.
46
   -- First, our depth adjustment is but a best attempt.
47
   -- Moreover, some characters may have a small depth of their own (ex. "O" in Gentium Plus)
48
   -- We must just ensure they stay within "reasonable bounds" with respect to the baseline,
49
   -- so as not to flow over the next lines.
50
   -- We compute a tolerance ratio based on the font metrics, expecting the font to be well-designed.
51
   -- The user can override the computation and set the dropcaps.bsratio setting manually.
52
   -- (LaTeX would likely approximate it using a \strut = with a depth ratio of 0.3bs)
53
   local bsratio
NEW
54
   if SILE.settings:get("dropcaps.bsratio") then
×
NEW
55
      bsratio = SILE.settings:get("dropcaps.bsratio")
×
NEW
56
      SU.debug("dropcaps", "Using user-defined descender baseline ratio", bsratio)
×
57
   else
NEW
58
      bsratio = computeBaselineRatio()
×
NEW
59
      SU.debug("dropcaps", "Using computed descender baseline ratio", bsratio)
×
60
   end
NEW
61
   return bsratio * SILE.types.measurement("1bs"):tonumber()
×
62
end
63

UNCOV
64
function package:registerCommands ()
×
65
   -- This implementation relies on the hangafter and hangindent features of Knuth's line-breaking algorithm.
66
   -- These features in core line breaking algorithm supply the blank space in the paragraph shape but don't fill it with anything.
NEW
67
   self:registerCommand("dropcap", function (options, content)
×
NEW
68
      local lines = SU.cast("integer", options.lines or 3)
×
NEW
69
      local join = SU.boolean(options.join, false)
×
NEW
70
      local standoff = SU.cast("measurement", options.standoff or "1spc")
×
NEW
71
      local raise = SU.cast("measurement", options.raise or 0)
×
NEW
72
      local shift = SU.cast("measurement", options.shift or 0)
×
NEW
73
      local size = SU.cast("measurement or nil", options.size or nil)
×
NEW
74
      local scale = SU.cast("number", options.scale or 1.0)
×
NEW
75
      local strict = SU.boolean(options.strict, true)
×
NEW
76
      if strict and options.depthadjust then
×
NEW
77
         SU.warn("The depthadjust option is ignored in strict mode.")
×
78
      end
NEW
79
      local color = options.color
×
80
      -- We need to measure the "would have been" size before using this.
NEW
81
      options.size = nil
×
82
      -- Clear irrelevant option values before passing to font and measuring content.
NEW
83
      options.lines, options.join, options.raise, options.shift, options.color, options.scale =
×
NEW
84
         nil, nil, nil, nil, nil, nil
×
85

NEW
86
      if color then
×
NEW
87
         self:loadPackage("color")
×
88
      end
89

90
      -- Some initial capital fonts have all their glyphs hanging below the baseline (e.g. EB Garamond Initials)
91
      -- We cannot manage all pathological cases.
92
      -- Quite empirically, we can shape character(s) which shouldn't usually have a depth normally.
93
      -- If it has, then likely all glyphs do also and we need to compensate for that everywhere.
NEW
94
      local depthadjust = options.depthadjust or "I"
×
NEW
95
      local depthAdjustment = not strict and shapeHbox(options, { depthadjust }).depth:tonumber() or 0
×
NEW
96
      SU.debug("dropcaps", "Depth adjustment", depthAdjustment)
×
97

98
      -- We want the drop cap to span over N lines, that is N - 1 baselineskip + the height of the first line.
99
      -- Note this only works for the default linespace mechanism.
100
      -- We determine the height of the first line by measuring the size the initial content *would have* been.
NEW
101
      local tmpHbox = shapeHbox(options, content)
×
NEW
102
      local extraHeight = SILE.types.measurement((lines - 1) .. "bs"):tonumber()
×
NEW
103
      local curHeight = tmpHbox.height:tonumber() + depthAdjustment
×
NEW
104
      local targetHeight = (curHeight - depthAdjustment) * scale + extraHeight
×
NEW
105
      if strict then
×
106
         -- Take into account the compensated depth of the initial
NEW
107
         curHeight = curHeight + tmpHbox.depth:tonumber()
×
108
      end
NEW
109
      SU.debug("dropcaps", "Target height", targetHeight)
×
110

111
      -- Now we need to figure out how to scale the dropcap font to get an initial of targetHeight.
112
      -- From that we can also figure out the width it will be at that height.
NEW
113
      local curSize = SILE.types.measurement(SILE.settings:get("font.size")):tonumber()
×
NEW
114
      local curWidth = tmpHbox.width:tonumber()
×
NEW
115
      options.size = size and size:tonumber() or (targetHeight / curHeight * curSize)
×
NEW
116
      local targetWidth = curWidth / curSize * options.size
×
NEW
117
      SU.debug("dropcaps", "Target font size", options.size)
×
NEW
118
      SU.debug("dropcaps", "Target width", targetWidth)
×
119

120
      -- Typeset the dropcap with its final shape, but don't output it yet.
NEW
121
      local hbox = shapeHbox(options, content)
×
122

NEW
123
      if not strict then
×
124
         -- Compensation for regular extra depth.
NEW
125
         local compensationHeight = depthAdjustment * options.size / curSize
×
NEW
126
         SU.debug("dropcaps", "Compensation height", compensationHeight)
×
127

128
         -- Some fonts have descenders on letters such as Q, J, etc.
129
         -- In that case we may need extra lines to the dropcap.
NEW
130
         local extraDepth = hbox.depth:tonumber() - compensationHeight
×
NEW
131
         local toleranceDepth = getToleranceDepth()
×
NEW
132
         if extraDepth > toleranceDepth then
×
NEW
133
            SU.debug("dropcaps", "Extra depth", extraDepth, "> tolerance", toleranceDepth)
×
NEW
134
            local extraLines = math.ceil((extraDepth - toleranceDepth) / SILE.types.measurement("1bs"):tonumber())
×
NEW
135
            lines = lines + extraLines
×
NEW
136
            SU.debug("dropcaps", "Extra lines needed to fit", extraLines)
×
137
         else
NEW
138
            SU.debug("dropcaps", "Extra depth", extraDepth, "< tolerance", toleranceDepth)
×
139
         end
NEW
140
         raise = raise:tonumber() + compensationHeight
×
141
      else
NEW
142
         raise = raise:tonumber() + hbox.depth:tonumber()
×
143
      end
144

145
      -- Setup up the necessary indents for the final paragraph content
NEW
146
      local joinOffset = join and standoff:tonumber() or 0
×
NEW
147
      SILE.settings:set("current.hangAfter", -lines)
×
NEW
148
      SILE.settings:set("current.hangIndent", targetWidth + joinOffset)
×
NEW
149
      SILE.call("noindent")
×
NEW
150
      SU.debug("dropcaps", "joinOffset", joinOffset)
×
151

152
      -- The paragraph is indented so as to leave enough space for the drop cap.
153
      -- We "trick" the typesetter with a zero-dimension box wrapping our original box.
NEW
154
      SILE.call("rebox", { height = 0, depth = 0, width = -joinOffset }, function ()
×
NEW
155
         SILE.call("glue", { width = shift - targetWidth - joinOffset })
×
NEW
156
         SILE.call("lower", { height = extraHeight - raise }, function ()
×
NEW
157
            SILE.call(color and "color" or "noop", { color = color }, function ()
×
NEW
158
               SILE.typesetter:pushHbox(hbox)
×
159
            end)
160
         end)
161
      end)
NEW
162
   end, "Show an 'initial capital' (also known as a 'drop cap') at the start of the content paragraph.")
×
163
end
164

165
package.documentation = [[
166
\begin{document}
167
\use[module=packages.dropcaps]
168
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.
169

170
It provides the \autodoc:command{\dropcap} command.
171
The content passed will be the initial character(s).
172
The primary option is \autodoc:parameter{lines}, an integer specifying the number of lines to span (defaults to \code{3}).
173
The scale of the characters can be adjusted relative to the first line using the \autodoc:parameter{scale} option (defaults to \code{1.0}).
174
The \autodoc:parameter{join} parameter is a boolean for whether to join the dropcap to the first line (defaults to \code{false}).
175
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.
176
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.
177
To tweak the position of the dropcap, measurements may be passed to the \autodoc:parameter{raise} and \autodoc:parameter{shift} options.
178
Other options passed to \autodoc:command{\dropcap} will be passed through to \autodoc:command{\font} when drawing the initial letter(s).
179
This may be useful for passing OpenType options or other font preferences.
180

181
Some fonts have capitals — such as, typically, \autodoc:example{Q} and \autodoc:example{J} — hanging below the baseline.
182
By default, the dropcap fits the specified number of lines and the characters are typeset in a smaller size to fit these descenders.
183

184
With the \autodoc:parameter{strict=false} option, the characters are scaled with respect to their height only, and extra hanged lines are added to the dropcap in order to accommodate the descenders.
185
The dropcap is allowed to overflow the baseline by a reasonable amount, before triggering the addition of extra lines, for fonts that have capitals very slightly hanging below the baseline.
186
This tolerance is computed based on the font metrics.
187
If you want to bypass this mechanism and adjust the tolerance, you can use the \autodoc:setting{dropcaps.bsratio} setting.
188

189
Moreover, some fonts, such as EB Garamond Initials, have \em{all} capitals hanging below the baseline.
190
To take this case into account in non-strict mode, the depth adjustment of the dropcap is empirically corrected based on that of a character which shouldn't have any, by default an \autodoc:example{I}.
191
The character(s) used for this depth adjustment correction can be specified using the \autodoc:parameter{depthadjust} option.
192

193
\begin{autodoc:note}
194
One caveat is that the size of the initials is calculated using the default linespacing mechanism.
195
If you are using an alternative method from the \autodoc:package{linespacing} package, you might see strange results.
196
Set the \autodoc:setting{document.baselineskip} to approximate your effective leading value for best results.
197
If that doesn't work set the size manually.
198
Using \code{SILE.setCommandDefaults()} can be helpful for so you don't have to set the size every time.
199
\end{autodoc:note}
200
\end{document}
UNCOV
201
]]
×
202

UNCOV
203
return package
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc