lapis icon indicating copy to clipboard operation
lapis copied to clipboard

Fully qualified calls to HTML generation functions

Open fosskers opened this issue 1 year ago • 3 comments

Hi there, thanks for Lapis. I'm testing it out and it seems really cool.

I found this example for generating HTML from within Lua:

local MyApp = lapis.Application()

MyApp:match("/", function(self)
  return self:html(function()
    h1({class = "header"}, "Hello")
    div({class = "body"}, function()
      text "Welcome to my site!"
    end)
  end)
end)

I've always preferred this pattern over templates in other languages, so I'm happy that it's to be found here too.

I noticed though that the HTML functions like h1 and div aren't actually in scope here; they're not called as children of any imported Module table. I did a quick search of the Lapis code base and didn't find where they were actually defined (to say call them manually as lapis.h1 etc.).

Is there a way to call them in a "fully qualified way" like that? I saw also this comment in the Guide:

The environment of the function passed to self.html is set to one that support the HTML builder functions described above.

and was wondering about its implications. Thank you!

fosskers avatar Mar 31 '23 04:03 fosskers

the html method creates a dynamic function scope that lazily creates these functions as you call them. (For this to work there must be no local variable that is shadowing the name of the function you want to call).

This is the exact line where the HTML tag function is generated: https://github.com/leafo/lapis/blob/master/lapis/html.moon#L180

Because these functions are bound to the background buffer that is accumulating the output, there is no way to access these function objects outside of the special rendering scope.

Does that help? If you could tell me more about what you're trying to do I might have a better answer for you.

leafo avatar Apr 18 '23 03:04 leafo

Thanks! To the matter at hand then: I'm trying to use Lapis with Fennel and it seems that in its Fennel->Lua translation phase, it strictly considers which function symbols exist and which don't (rejecting the latter).

I have in a app.lua:

local app = require("fennel").install().dofile("app.fnl")

return app

followed by this in Fennel:

(local lapis (require :lapis))
(local app (lapis.Application))


(fn index [self]
  (self:html (fn []
               (h1 {:class "header"} "Hello")
               (div {:class "body"} "Welcome to my site!"))))

(app:match "/" index)

app

My server starts fine with lapis server, but attempting to access localhost:8080/ gives this in my terminal:

2023/04/18 13:06:22 [error] 63794#0: *1 lua entry thread aborted: runtime error: app.fnl:10:16 Compile error: unknown identifier: h1

               (h1 {:class "header"} "Hello")
* Try looking to see if there's a typo.
* Try using the _G table instead, eg. _G.h1 if you really want a global.
* Try moving this code to somewhere that h1 is in scope.
* Try binding h1 as a local in the scope of this code.
stack traceback:
coroutine 0:
        [C]: in function 'require'
        /home/colin/.luarocks/share/lua/5.1/lapis/init.lua:15: in function 'serve'
        content_by_lua(nginx.conf.compiled:26):2: in main chunk, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", host: "localhost:8080"
2023/04/18 13:06:22 [error] 63794#0: *2 open() "/home/colin/code/fennel/lapit-test/static/favicon.ico" failed (2: No such file or directory), client: 127.0.0.1, server: , request: "GET /favicon.ico HTTP/1.1", host: "localhost:8080", referrer: "http://localhost:8080/"

It (re: the Fennel compiler) is unable to find the h1 , since it doesn't exist yet as you explained.

Any thoughts of how I could approach this better?

fosskers avatar Apr 18 '23 04:04 fosskers

@fosskers I had the same problem, and found the solution in this comment: https://github.com/leafo/lapis/issues/259#issuecomment-543876041

We need to let Fennel know that these globals [will] exist. To make your example work, I modified app.lua like this:

local globals = {"h1", "div"}

for key, _ in pairs(_G) do
    table.insert(globals, key)
end

local app = require("fennel").install().dofile("app.fnl", {allowedGlobals = globals})

return app

EDIT: Unfortunately the above broke fennel.view for me. In the end I reached for a heavier hammer:

local app = require("fennel").install().dofile("app.fnl", {allowedGlobals = false})

return app

sarna avatar Jan 27 '24 12:01 sarna