Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Implement hangafter/hangindent + dropcaps package #1219

Merged
merged 17 commits into from
Oct 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5417189
feat(core): Implement Knuth's hangAfter and hangIndent
Omikhleia Sep 13, 2021
09dd30a
test(core): Add hangafter/hangbefore regression test
Omikhleia Sep 13, 2021
cb9105a
feat(packages): Add dropcaps package
Omikhleia Sep 13, 2021
0354b58
Merge remote-tracking branch 'upstream/master' into knuth-hangafter-d…
alerque Sep 24, 2021
b5bf110
chore(packages): Overhaul dropcaps package
alerque Sep 23, 2021
0b30279
test(core): Overhaul hanging indents test, add expectations
alerque Sep 23, 2021
e612849
chore(packages): Redo dropcaps input to use content as initial
alerque Sep 25, 2021
0a88948
feat(packages): Add shift, raise, and size options to dropcaps
alerque Sep 25, 2021
7263148
chore(packages): Add standoff parameter to adjust join size if used
alerque Sep 25, 2021
18ee23b
feat(core): Implement paragraph duration hanging indent settings
alerque Sep 27, 2021
2dfbacc
chore(typesetter): Make sure regular {r,l}skip values work when no ha…
alerque Sep 27, 2021
2b04507
feat(classes): Add \noop function for versatile SILE.call() use
alerque Sep 27, 2021
d042bcf
feat(packages): Implement color option for dropcaps
alerque Sep 27, 2021
8db784d
docs(packages): Document dropcap sizing caveat when using alternative…
alerque Sep 28, 2021
81c5db9
Merge branch 'master' into knuth-hangafter-dropcaps
alerque Sep 29, 2021
682b2fd
chore(packages): Add scale factor so dropcaps are more ergonomic
alerque Sep 29, 2021
1b8bffb
chore(packages): Blacklist footnotes/folios from inheriting unlikely …
alerque Sep 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions core/baseclass.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ SILE.registerCommand("define", function (options, content)
end, options.help, SILE.currentlyProcessingFile)
end, "Define a new macro. \\define[command=example]{ ... \\process }")

-- A utility function that allows SILE.call() to be used as a noop wrapper.
SILE.registerCommand("noop", function (_, content)
SILE.process(content)
end)

SILE.registerCommand("comment", function (_, _)
end, "Ignores any text within this command's body.")

Expand Down Expand Up @@ -218,6 +223,18 @@ SILE.baseClass = std.object {
default = nil,
help = "Glue at start of paragraph"
})
SILE.settings.declare({
parameter = "current.hangIndent",
type = "integer or nil",
default = nil,
help = "Size of hanging indent"
})
SILE.settings.declare({
parameter = "current.hangAfter",
type = "integer or nil",
default = nil,
help = "Number of lines affected by handIndent"
})
SILE.outputter:init(self)
self:registerCommands()
-- Call all stored package init routines
Expand Down Expand Up @@ -297,10 +314,26 @@ SILE.baseClass = std.object {
newPar = function (typesetter)
typesetter:pushGlue(SILE.settings.get("current.parindent") or SILE.settings.get("document.parindent"))
SILE.settings.set("current.parindent", nil)
local hangIndent = SILE.settings.get("current.hangIndent")
if hangIndent then
SILE.settings.set("linebreak.hangIndent", hangIndent)
end
local hangAfter = SILE.settings.get("current.hangAfter")
if hangAfter then
SILE.settings.set("linebreak.hangAfter", hangAfter)
end
end,

endPar = function (typesetter)
typesetter:pushVglue(SILE.settings.get("document.parskip"))
if SILE.settings.get("current.hangIndent") then
SILE.settings.set("current.hangIndent", nil)
SILE.settings.set("linebreak.hangIndent", nil)
end
if SILE.settings.get("current.hangAfter") then
SILE.settings.set("current.hangAfter", nil)
SILE.settings.set("linebreak.hangAfter", nil)
end
end,

options = {
Expand Down
64 changes: 55 additions & 9 deletions core/break.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
SILE.settings.declare({ parameter = "linebreak.parShape", type = "string or nil", default = nil }) -- unimplemented
SILE.settings.declare({ parameter = "linebreak.tolerance", type = "integer or nil", default = 500 })
SILE.settings.declare({ parameter = "linebreak.pretolerance", type = "integer or nil", default = 100 })
SILE.settings.declare({ parameter = "linebreak.hangIndent", type = "nil", default = nil }) -- unimplemented
SILE.settings.declare({ parameter = "linebreak.hangIndent", type = "integer or nil", default = nil })
SILE.settings.declare({ parameter = "linebreak.hangAfter", type = "integer or nil", default = nil })
SILE.settings.declare({ parameter = "linebreak.adjdemerits", type = "integer", default = 10000,
help = "Additional demerits which are accumulated in the course of paragraph building when two consecutive lines are visually incompatible. In these cases, one line is built with much space for justification, and the other one with little space." })
SILE.settings.declare({ parameter = "linebreak.looseness", type = "integer", default = 0 })
Expand Down Expand Up @@ -75,20 +76,32 @@ end

function lineBreak:setupLineLengths() -- 874
self.parShape = param("parShape")
self.hangAfter = param("hangAfter") or 0
self.hangIndent = param("hangIndent") or 0
if not self.parShape then
if not param("hangIndent") then
if self.hangIndent == 0 then
self.lastSpecialLine = 0
self.secondWidth = self.hsize or SU.error("No hsize")
else
SU.error("Hanging indents not supported yet. Implement node 875 from TeX!", true)
else -- 875
self.lastSpecialLine = math.abs(self.hangAfter)
if self.hangAfter < 0 then
self.secondWidth = self.hsize or SU.error("No hsize")
self.secondIndent = 0
self.firstWidth = self.hsize - math.abs(self.hangIndent)
self.firstIndent = self.hangIndent -- keep the signed value for later
else
self.firstWidth = self.hsize or SU.error("No hsize")
self.firstIndent = 0
self.secondWidth = self.hsize - math.abs(self.hangIndent)
self.secondIndent = self.hangIndent -- keep the signed value for later
end
end
else
self.lastSpecialLine = #param("parShape")
self.secondWidth = SU.error("Oops")
end
if param("looseness") == 0 then self.easy_line = self.lastSpecialLine else self.easy_line = awful_bad end
-- self.easy_line = awful_bad

end

function lineBreak:tryBreak() -- 855
Expand Down Expand Up @@ -131,8 +144,15 @@ function lineBreak:tryBreak() -- 855
self.old_l = awful_bad -1
else
self.old_l = self.r.lineNumber
if self.r.lineNumber > self.lastSpecialLine then self.lineWidth = self.secondWidth
elseif not self.parShape then self.lineWidth = self.firstWidth else self.lineWidth = self.parShape[self.r.lineNumber]
if self.r.lineNumber > self.lastSpecialLine then
self.lineWidth = self.secondWidth
self.lineIndent = self.secondIndent
elseif not self.parShape then
self.lineWidth = self.firstWidth
self.lineIndent = self.fistIndent
else
self.lineWidth = self.parShape[self.r.lineNumber]
--FIXME self.lineIndent: parShape needs it too (its supposed to l1 i1 l2 i2... lN iN).
end
end
if debugging then SU.debug("break", "line width = "..self.lineWidth) end
Expand Down Expand Up @@ -272,7 +292,10 @@ function lineBreak:deactivateR() -- 886
if self.prev_r.type == "delta" then
self.r = self.prev_r.next
if self.r == self.activeListHead then
self.curActiveWidth:___sub(self.r.width)
self.curActiveWidth:___sub(self.prev_r.width)
-- FIXME It was crashing here, so changed from:
-- self.curActiveWidth:___sub(self.r.width)
-- But I'm not so sure reading Knuth here...
self.prev_prev_r.next = self.activeListHead
self.prev_r = self.prev_prev_r
elseif self.r.type == "delta" then
Expand Down Expand Up @@ -566,10 +589,33 @@ function lineBreak:postLineBreak() -- 903
local p = self.bestBet
local breaks = {}
local line = 1

local nbLines = 0
local p2 = p
repeat
nbLines = nbLines + 1
p2 = p2.prevBreak
until not p2

repeat
-- local width
local indent
-- ParShape case should be handled too here.
if self.hangAfter > 0 then
-- width = line > nbLines - self.hangAfter and self.firstWidth or self.secondWidth
indent = line > nbLines - self.hangAfter and self.firstIndent or self.secondIndent
elseif self.hangAfter == 0 then
-- width = self.hsize
indent = 0
else
-- width = line > nbLines + self.hangAfter and self.firstWidth or self.secondWidth
indent = line > nbLines + self.hangAfter and self.firstIndent or self.secondIndent
end

table.insert(breaks, 1, {
position = p.curBreak,
width = self.parShape and self.parShape[line] or self.hsize
width = self.parShape and self.parShape[line] or self.hsize,
indent = indent
})
if p.alternates then
for i = 1, #p.alternates do
Expand Down
11 changes: 9 additions & 2 deletions core/typesetter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -601,16 +601,22 @@ SILE.defaultTypesetter = std.object {
end
end,

addrlskip = function (self, slice, margins)
addrlskip = function (self, slice, margins, hangIndent)
local LTR = self.frame:writingDirection() == "LTR"
local rskip = margins[LTR and "rskip" or "lskip"]
if not rskip then rskip = SILE.nodefactory.glue(0) end
if hangIndent and hangIndent < 0 then
rskip = SILE.nodefactory.glue({ width = rskip.width:tonumber() - hangIndent })
end
rskip.value = "margin"
-- while slice[#slice].discardable do table.remove(slice, #slice) end
table.insert(slice, rskip)
table.insert(slice, SILE.nodefactory.zerohbox())
local lskip = margins[LTR and "lskip" or "rskip"]
if not lskip then lskip = SILE.nodefactory.glue(0) end
if hangIndent and hangIndent > 0 then
lskip = SILE.nodefactory.glue({ width = lskip.width:tonumber() + hangIndent })
end
lskip.value = "margin"
while slice[1].discardable do table.remove(slice, 1) end
table.insert(slice, 1, lskip)
Expand All @@ -636,7 +642,8 @@ SILE.defaultTypesetter = std.object {
end
end
if seenHbox == 0 then break end
self:addrlskip(slice, self:getMargins())
local mrg = self:getMargins()
self:addrlskip(slice, mrg, point.indent)
local ratio = self:computeLineRatio(point.width, slice)
-- TODO see bug 620
-- if math.abs(ratio) > 1 then SU.warn("Using ratio larger than 1" .. ratio) end
Expand Down
94 changes: 94 additions & 0 deletions packages/dropcaps.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
SILE.require("packages/rebox")
SILE.require("packages/raiselower")

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.call("hbox", {}, function ()
SILE.call("font", options, content)
end)
table.remove(SILE.typesetter.state.nodes)
return hbox
end

-- 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.
SILE.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 color = options.color
options.size = nil -- we need to measure the "would have been" size before using this

if color then SILE.require("packages/color") end

-- 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.
-- This gives the font some control over its relative size, sometimes desired sometimes undesired.
local tmpHbox = shapeHbox(options, content)
local extraHeight = SILE.measurement((lines - 1).."bs"):tonumber()
local targetHeight = tmpHbox.height:tonumber() * scale + extraHeight
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 curHeight, curWidth = tmpHbox.height:tonumber(), tmpHbox.width:tonumber()
options.size = size and size:tonumber() or (targetHeight / curHeight * 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)

-- 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, 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.")

return {
documentation = [[
\begin{document}
The \code{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 \code{\\dropcap} command.
The content passed will be the initial character(s).
The primary option is \code{lines}, an integer specifying the number of lines to span (defaults to 3).
The scale of can be adjusted relative to the first line using the \code{scale} option (defaults to 1.0).
The \code{join} is a boolean for whether to join the dropcap to the first line (defaults to false).
If \code{join} is true, the value of the \code{standoff} option (defaults to 1spc) is applied to all but the first line.
Optionally \code{color} can be passed to change the typeface color, sometimes useful to offset the apparent weight of a large glyph.
To tweak the position of the dropcap, measurements may be passed to the \code{raise} and \code{shift} options.
Other options passed to \\dropcap will be passed through to \\font when drawing the initial letter(s).
This may be useful for passing OpenType options or other font preferences.

\begin{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 linespacing package, you might see strange results.
Set the \code{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{note}
\end{document}
]] }
11 changes: 11 additions & 0 deletions packages/folio.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ return {
SILE.settings.pushState()
-- Restore the settings to the top of the queue, which should be the document #986
SILE.settings.toplevelState()

-- Reset settings the document may have but should not be applied to footnotes
-- See also same resets in footnote package
for _, v in ipairs({
"current.hangAfter",
"current.hangIndent",
"linebreak.hangAfter",
"linebreak.hangIndent" }) do
SILE.settings.set(v, SILE.settings.defaults[v])
end

SILE.call("foliostyle", {}, { SILE.formatCounter(SILE.scratch.counters.folio) })
SILE.typesetter:leaveHmode()
SILE.settings.popState()
Expand Down
10 changes: 10 additions & 0 deletions packages/footnotes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ SILE.registerCommand("footnote", function (options, content)
-- Restore the settings to the top of the queue, which should be the document #986
SILE.settings.toplevelState()

-- Reset settings the document may have but should not be applied to footnotes
-- See also same resets in folio package
for _, v in ipairs({
"current.hangAfter",
"current.hangIndent",
"linebreak.hangAfter",
"linebreak.hangIndent" }) do
SILE.settings.set(v, SILE.settings.defaults[v])
end

-- Apply the font before boxing, so relative baselineskip applies #1027
local material
SILE.call("footnote:font", {}, function ()
Expand Down
Loading