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

Floats need improvement, esp in drop cap use case #394

Open
alerque opened this issue Nov 3, 2016 · 1 comment
Open

Floats need improvement, esp in drop cap use case #394

alerque opened this issue Nov 3, 2016 · 1 comment
Labels
enhancement Software improvement or feature request

Comments

@alerque
Copy link
Member

alerque commented Nov 3, 2016

Intro

I've had a couple books that use drop-caps at the start of chapters or sections. So far I've hacked around it a couple ways to get the projects out the door, but the result has been brittle and it seems like some functionality is missing.

Finding the right place to even apply drop-cap code

Right out of the gate, figuring out when to apply such a style and getting the right content or letter wrapped is hard. This isn't something I want encoded in my documents which are in Markdown and get converted to other formats as well. I need to do it automatically.

One way has been to overload the chapter() or similar command to trigger the style of the next content to come along. In the case of a drop cap letter I'm doing something like this:

local inputfilter = SILE.require("packages/inputfilter").exports
local originalTypesetter = SILE.typesetter.typeset
dropcapNextLetter = function ()
  SILE.typesetter.typeset = function (self, text)
    local first, rest = text:match("([^%w]*%w)(.*)")
    if load and first and rest then
      SILE.typesetter.typeset = originalTypesetter
      SILE.call("dropcap", {}, {first})
      SILE.typesetter.typeset(self, rest)
    else
      originalTypesetter(self, text)
    end
  end
end

SILE.registerCommand("chapter", function (options, content)
   ...
    dropcapNextLetter()
end)

This hijacks the typesetter the text time it is asked to output something, strips off the first character, sends it to the dropcap function, then goes on with the rest.

This works but it feels dirty and shouldn't be something SILE users should be required to come up with. If it is the right way of doing this let me know and I'll work it up and submit it as a package. If it's the wrong way to go about this I'd love to hear it.

The other way I've tackled this is when something decorative is being placed in leu of a drop-cap. In this case I've hijacked the typesetter functions a little differently:

SILE.scratch.lastWasRef = false

SILE.registerCommand("call-for-chaptercap", function (options, content)
  SILE.scratch.lastWasRef = true
  SILE.call("noindent")
end)

local push = SILE.typesetter.pushHorizontal
SILE.typesetter.pushHorizontal = function (self, node)
  if SILE.scratch.lastWasRef then
    SILE.call("output-chaptercap")
  end
  return push(self, node)
end

SILE.registerCommand("output-chaptercap", function (options, content)
  SILE.scratch.lastWasRef = false
  ...
end)

In this case I'm setting a scratch variable after the appropriate section headings and hijacking the function that pushes unshaped horizontal stuff on the queue to add stuff my own content in there the first time it tries to spit out a line. This doesn't seem quite as dirty as the input filter thing, but still requires a lot of knowledge about the core typesetter to pull off.

It seems like there should be some more general way to munge the document itself after it has been parsed during typesetting to pull this sort of thing off.

The actual drop-cap code

Even after a function is running at the right place, actually typesetting a drop cap is not as easy as the manual makes it sound. My simplest invocation is something like this:

SILE.registerCommand("dropcap", function (options, content)
  SILE.call("noindent")
  SILE.call("float", { bottomboundary = "1.2ex", rightboundary = "1spc" }, function ()
    SILE.call("book:chapterfont", { size = "5.2ex", weight = 800 }, content)
  end)
  SILE.call("indent")
end)

Sample output:

selection_291

This looks nice enough, but it's deceptively tricky to use because the metrics don't stay put. There is no generalize way to make the dropcap line up with anything. Even after you tailor the metrics to match the font size of the document, how many lines you want it to span, and what the padding should be, it breaks if you fiddle with the document. Any change in the leading or font metrics bork the alignment. The worst is the frame bottom boundary because it determines the leading between the lines next to and below the drop cap. The document leading calculations are not respected and that line may end up with more or less space that it would normally have had. This can even vary from chapter to chapter depending on ascender/descender status. I ended up using a fixed line-spacing for one project just because the drop caps were too unpredictable otherwise.

Somehow the frame logic in float() needs to match the leading that would have otherwise been there without the frame.

Secondarily I've played around with another way of injecting drop caps. This one is dirty and I know it but it was interesting as a proof of concept. It only works on a lark in about 80% of cases, but depending on how many passes the line-breaking algorithm makes it can also get out of whack. The nice thing is that because it doesn't mess with frames the leading never gets off, just the rskip.

You've been warned:

SILE.settings.declare({ name="linebreak.templskip", type = "Glue or nil", default = nil})
SILE.scratch.templskip = 0

local get = SILE.settings.get
SILE.settings.get = function(name)
  if name == "document.lskip" and SILE.scratch.templskip > 0 then
    name = "linebreak.templskip"
    SILE.scratch.templskip = SILE.scratch.templskip - 1
  end
  return get(name)
end

SILE.registerCommand("output-chaptercap", function (options, content)
  SILE.call("rebox", {  height = 0, width = 0 }, function ()
    SILE.call("lower", { height = "1.5em"}, function ()
      SILE.call("glue", { width = "-3em" })
      SILE.call("font", { family = "Symbola", size = "3em" } , { "" })
    end)
  end)
  SILE.scratch.templskip = 3
  SILE.settings.set("linebreak.templskip", SILE.nodefactory.newGlue("3em"))
end)

Yes that makes off with the settings get() function and makes it return different values depending on how many times it's been called recently.

Sample output:

selection_290

When the output eats crow:

selection_292

Related problems

Incidentally this last use case is similar to my problem in #318 that I hacked around yet another way. The major difference there was that line breaks were known entities and there was no flowing that needed to happen. Hence I could work up a paragraph based solution.

Conclusion

That's now 3 ways I've hacked around a similar problem with about half a dozen variants of one of them. I wouldn't wish this on any future users for doing something as simple as typesetting clean drop-caps.

What should we do?

@alerque alerque added the enhancement Software improvement or feature request label Nov 3, 2016
@alerque
Copy link
Member Author

alerque commented Nov 3, 2016

@khaledhosny In the last image above just where I cropped it off there are still some descenders visible from the previous line. Do any of those squiggles ring a bell?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Software improvement or feature request
Projects
None yet
Development

No branches or pull requests

1 participant