To prepare myself for the transition to lua and practice some syntax along the way (and maybe out of boredom), I recently decided to implement a simple link-preview widget using the Open Graph protocol properties found in many websites.
My code consists of two functions: og.fetch, wich extracts tags and their properties from a website
and og.card, wich uses the data returned by og.fetch to display an info card in plain html.
This is my first time writing lua and I’m sure there’s lots of room for improvement, but at least it’s self-contained and works (so far xD).
space-lua:
og = {}
function og.fetch(url)
local function extract_prop(header, prop_name)
local prop_start, prop_end = string.find(header, 'og:'..prop_name..'" content="[^"]+"/?>')
local prop = string.sub(header, prop_start, prop_end)
local tag_closer_length = 0
if string.find(prop, "/>") == nil then
-- <tag prop="value">
tag_closer_length = 2
else
-- <tag prop="value"/>
tag_closer_length = 3
end
return string.sub(prop, 15 + string.len(prop_name), prop_end - prop_start - tag_closer_length)
end
local response = http.request(url)
if response.ok then
local body = response.body
local head_start, _ = string.find(body, "<head>")
local head_end, _ = string.find(body, "</head>")
local header = string.sub(body, head_start, head_end)
local title = extract_prop(header, "title")
local url = extract_prop(header, "url")
local image = extract_prop(header, "image")
-- description and site_name are optional properties, might return nil/""
local description = extract_prop(header, "description")
local sitename = extract_prop(header, "site_name")
return {title = title, url = url, image = image, description = description, sitename = sitename}
else
return "Error: got status code "..response.status
end
end
function og.card(url, width)
local og_data = og.fetch(url)
return widget.new {
html = "<div style='background-color:rgba(0,0,0,0.15);padding:10px;border-radius:5px;display:flex;flex-direction:column;align-items:space-evenly;width:"..width.."px;'><span class='sb-sub'>"..og_data.sitename.."</span><a href = '"..og_data.url.."'><b>"..og_data.title.."</b></a>"..og_data.description.."<img src='"..og_data.image.."' style='margin-top:10px;'/></div>"
}
end
Nice, thanks for sharing. It’s good to see what people are doing with this. Part of the Lua (and SilverBullet) fun is that it’s perfectly fine to hack things so that they work for you, without overthinking too much about how to “properly” engineer. It’s ok, chill
That said, I am wondering whether parsing HTML is going to be a common use case. If so, there’s probably ways (already) that can expose some of (browser) JavaScript native DOM handling and querying to Space Lua as well to make this nicer.
My first thought was to just use the DOMParser class with a call to js.new, but I ended up implementing it in 100% Lua just to see how much more work that would be.
Looking at the code now, I feel like the effort isn’t immensly higher than using native DOM parsing (though that probably depends on the application), but having a lua api available would certainly improve parsing stability & scalability a lot
Edit: just realized, that using browser-native apis with js.new is not really possible (please correct me if I’m wrong), but using something like dom-parser with js.import would probably work as well.
widget.new should do the trick, I think I wrote this while still on v1, where the api was never updated to this syntax.
I even wrote an improved version some time ago that makes use of js.window.DOMParser, making the whole thing a lot faster and more reliable. I don’t know why I didn’t share the code back then, but for anyone still looking to use this widget in v2, here’s my new version:
og = {}
function og.fetch(url)
local response = http.request(url)
if response.ok then
local parser = js.new(js.window.DOMParser)
local document = parser.parseFromString(response.body, "text/html")
local og_properties = {}
for meta in each(document.getElementsByTagName("meta")) do
local prop = tostring(meta.getAttribute("property"))
if prop:startsWith("og:") then
og_properties[prop:sub(4)] = meta.getAttribute("content")
end
end
return og_properties
else
return { error = "Status code "..response.status }
end
end
function og.card(url, width)
width = width or 350
local og = og.fetch(url)
if og.error then return widget.markdown("==OG Error: "..og.error.."==") end
return widget.html(dom.div {
style = "width:"..width.."px;background-color:rgba(0,0,0,0.15);padding:3px 10px;border-radius:5px;",
dom.sub { og.site or og.site_name }, dom.br {},
dom.h3 { dom.a { href = og.url, og.title } },
dom.p { og.description or "" },
dom.img {
style = "width:"..width.."px;border-radius:5px;",
src = og.image
}
})
end