sile-typesetter / sile

The SILE Typesetter — Simon’s Improved Layout Engine
https://sile-typesetter.org
MIT License
1.65k stars 98 forks source link

Is there anything like a start hook? #2083

Closed jodros closed 1 month ago

jodros commented 3 months ago

I mean, something just like class:finish but to be run as the first thing just after the class is initialized?

I've tried self:registerPostinit but it doesn't seem to be what I thought it was for... BTW its actual function deserves to be in the documentation...

alerque commented 2 months ago

The class:_init() function is where such a hook would run ... if such a thing were possible. The reason there isn't such a hook is because there is nowhere¹ to inject code to actually use such a hook. The class tag is necessarily the outermost tag in a document, and the class initializes as the very first step.

  1. If what you need is something to run after the class initializes and before any document processing is done, then class:registerPostinit() is the right place.
  2. There should be very few things you wouldn't just be able to place as a \use statement at the start of your document. What is the actual use case here?
  3. If neither №1 or №2 is actually viable, the approach I would take would probably be creating a custom class that derives from whatever it was your were using, but that gives you the freedom to order things in the :_init() initializer any way you want, running things before or after the original init or replacing it entirely.

¹ Technically it could be taken from code or modules specified in CLI args, but that isn't very ergonomic and hence has not been pursued.

jodros commented 2 months ago

What is the actual use case here?

For example, the command \footnote:separator may be called in the document, usually in its beginning, right? But I don't want to have to write it every time, so I thought in calling it in the class:

    self:registerPostinit(function()
      SILE.call("footnote:separator", {}, function ()
        SILE.call("noindent")
        SILE.call("hrule", { width = "5cm" })
        SILE.call("bigskip")
      end)
    end)

I got Error: runtime error: /usr/local/share/sile/typesetters/base.lua:347: attempt to index field 'documentClass' (a nil value), so that's why I want a place to put such commands that need to be run after class initialization and cannot be in class:finish().

alerque commented 1 month ago

I see the issue in your use case.

This is poor design on SILE's part on an least two counts:

  1. The SILE.documentState.documentClass scoping is only marginally less egregious than SILE.scratch.…. Long terms this really needs to be refactored such that the SILE instance state is kept inside the actual instance of the thing running, not in some random global variable.

    flowchart LR
        S[SILE Instance] --> I[inputter instance]
        I --> C[class instance]
        C --> P(package instances)
  2. Configuration things like setting up footnotes styles should never have been done by trying to call and use the currently active typesetter. If using content processing is really necessary they should have been closures that run on use, not when being set. The class should be able to configure how footnotes look and should not be trying to all and use the actual typesetter!

As an end user you can work around this by using preambles instead. Here is an MWE. You'll have a class file classes/post.lua that look like this:

local book = require("classes.book")

local class = pl.class(book)
class._name = "post"

function class:_init (options)
book._init(self, options)
self:loadPackage("rules")
table.insert(SILE.input.preambles, function ()
    SILE.call("footnote:separator", {}, function ()
        SILE.call("noindent")
        SILE.call("hrule", { width = "30%lw", height = "2pt" })
        SILE.call("bigskip")
    end)
end)
end

return class

And here is a test document:

\begin[class=post,papersize=a6,landscape=true]{document}
\nofolios
\neverindent
\font[size=40pt]
Foo bar.\footnote{Haz bar?}
\end{document}

image

Given than whan is being done here is fundamentally content processing it does seem like a reasonable use of preambles as long as we don't have the above two issues fixed.

alerque commented 1 month ago

Another weird and possible fundamentally wrong way to work around this based on your original use case:

-self:registerPostinit(function()
+self:registerPostinit(function(class_)
+   SILE.documentState.documentClass = class_
    SILE.call("footnote:separator", {}, function ()
       SILE.call("noindent")
       SILE.call("hrule", { width = "5cm" })
       SILE.call("bigskip")
    end)
 end)

For this particular use case that would work, but preambles are probably a better solution. I'm still not sure about the fix on the SILE side in #2095 — something feels off about trying to fix this by allowing content processing at all from the class init.

Thoughts @Omikhleia? Are there other use cases that would not make sense as preambles (i.e. they are not fundamentally processing (or pretending to process) content that would make sense in deferred init that also needs access to the instantialized global namespace?

Omikhleia commented 1 month ago

I'm afraid it's really indeed hard to tell in a generic way. In this very example, for the footnote separator.... one might typically want the rule width to be a proportion of the footnote frame width (e.g. 20%, rather than some 5cm), but that's only available once frame sets have been evaluated -- and those might even change during the course of the document... In other terms, I am not sure the way the footnote separator is currently implemented (storing a pre-evaluated vbox) is very robust in the long term, independently from the current "page style". It might need to be truly "deferred" regarding when and how it is evaluated and built -- so we might just be looking at the wrong side of things trying to enforce it this way now, but I can't figure the whole picture. (We do know that the frame implementation has some flaws, but all my attempts fixing those I was concerned with ended up being stuck by the complex interactions between the typesetter, the page builder, insertions and the frame logic -- i.e. touching one thing easily breaks another).

alerque commented 1 month ago

Agreed. I don't even know if those hrule or skip metrics are getting absolutized on each use or at the start. I think the whole concept of using a function that returns nodes and stashing them is broken, the footnote separator should be a callback function (or just a function that gets called), not some pre-cached set of nodes generated by a typesetter that happened to be around at the start of the document.

I'm tempted to leave well enough along and not implement the fix in my PR, but that would be mostly based on this use case. This use case I think is more sensibly served with a preamble anyway. The lingering question is whether or not there are other viable use cases, and just from a logical standpoint I would expect anything run from the class:_post_init() (which is what our registerPostinit() callback registration function accomplishes) should be able to reach the class the same way anything run later in the document scope to reach it.