From ff0d799cf0e5aaa23596cb27153738ce7add16ec Mon Sep 17 00:00:00 2001 From: alyx Date: Sat, 6 Apr 2024 18:18:35 -0400 Subject: [PATCH] Document lua, fix small theme bugs --- README.md | 189 ++++++++++++++++++++++++++++---------- src/theming/hbs/plain.hbs | 4 +- src/theming/lua.rs | 9 +- 3 files changed, 151 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 084fce7..6117f0a 100644 --- a/README.md +++ b/README.md @@ -93,20 +93,20 @@ See `LFME_ALLOWLIST_REFRESH` for more info. ## Themes ### `LFME_THEME_DIR` -If set, must be a valid path to a directory containing [Handlebars](https://handlebarsjs.com/guide/#language-features) files, ending in LFME_THEME_EXT. +If set, must be a valid path to a directory containing a tree of [Handlebars](https://handlebarsjs.com/guide/#language-features) and [Lua](https://www.lua.org/) files, ending in `LFME_THEME_EXT_HBS` and `LFME_THEME_EXT_LUA`, respectively. They will be registered as themes on top of the builtin ones, with each theme's name being their filename minus the extension. Same-named themes will override builtin ones. See [Theming](#theming) for details on writing custom themes. -Theme names are the same as their path, minus the extension. Given an extension of .hbs, a directory like: +Theme names are the same as their path, minus the extension. A directory like: ``` themes/ mytheme.hbs - myothertheme.hbs + myothertheme.lua myunrelatedfile.css alices-themes/ - mytheme.hbs + mytheme.lua mysuperawesometheme.hbs ``` results in the following themes: @@ -119,16 +119,21 @@ alices-themes/mysuperawesometheme By default, these are loaded and compiled once, at startup. -### `LFME_THEME_EXT` (Default: `.hbs`) -The file extension for themes in `LFME_THEME_DIR`. +### `LFME_THEME_EXT_HBS` (Default: `.hbs`) +The file extension for handlebars themes in `LFME_THEME_DIR`. + +Note: This behaves more like a suffix than a file extension. You must include the leading dot for a file extension. + +### `LFME_THEME_EXT_LUA` (Default: `.lua`) +The file extension for lua themes in `LFME_THEME_DIR`. Note: This behaves more like a suffix than a file extension. You must include the leading dot for a file extension. ### `LFME_THEME_DEV` (Default: `0`) -If set to `1`, existing themes will be reloaded on edit. +If set to `1`, themes will be reloaded on edit. -Note: Even with this mode, adding a new theme requires a full reload. -Themes are only enumerated once, at startup. (This is a limitation of the [`handlebars`](https://docs.rs/handlebars/latest/handlebars) implementation in use, and may change.) +Note: Even with this mode, adding a new handlebars theme requires a full reload. +Handlebars hemes are only enumerated once, at startup. (This is a limitation of the [`handlebars`](https://docs.rs/handlebars/latest/handlebars) implementation in use, and may change.) ### `LFME_THEME_DEFAULT` (Default: `"plain"`) The theme to use when no query string is present. @@ -144,16 +149,48 @@ LFME_ALLOWLIST_MODE=exclusive LFME_ALLOWLIST=a_precious_basket_case, realRiversCuomo, Pixiesfan12345 ``` -# Theming {#theming} +# Theming -Custom themes are, as stated above, [Handlebars](https://handlebarsjs.com/guide/#language-features) files. +Custom themes are, as stated above, [Handlebars](https://handlebarsjs.com/guide/#language-features) and [Lua](https://www.lua.org/) files. They are expected to produce a complete HTML doc, which should contain info about a user's scrobbles. -See `src/themes` for examples of implementation. +You should have a passing familiarity with either language before reading below. + +## Handlebars Themes + +Handlebars is a simple templating language, and great for themes that don't do much complicated. +The builtin `plain` theme is written in handlebars, and any themes that want to just relay the basic scrobble info should be written in it. + +If you're looking to do more complicated effects, or need server-side computation, see below on writing [Lua Themes](#lua-themes). +Handlebars should __not__ be used if you're looking to do advanced effects. + +All builtin handlebars themes are located at [`/src/theming/hbs`](https://git.aleteoryx.me/cgit/lfm_embed/tree/src/theming/hbs). + +The following [helpers](https://handlebarsjs.com/guide/expressions.html#helpers) are provided, on top of the handlebars [builtins](https://handlebarsjs.com/guide/builtin-helpers.html): + +- `(eq Object Object) => Boolean`: Check equality between its args. + +- `(ne Object Object) => Boolean`: Check inequality between its args. + +- `(gt Number|String Number|String) => Boolean`: Check if the first arg is greater than the second. + +- `(gte Number|String Number|String) => Boolean`: Check if the first arg is greater than or equal to the second. + +- `(lt Number|String Number|String) => Boolean`: Check if the first arg is less than the second. -## Writing a Theme +- `(lte Number|String Number|String) => Boolean`: Check if the first arg is less than or equal to the second. -Themes should have, roughly, the structure below: +- `(and Boolean Boolean) => Boolean`: Boolean AND gate. + +- `(or Boolean Boolean) => Boolean`: Boolean OR gate. + +- `(not Boolean) => Boolean`: Boolean NOT gate. + +- `(uri_encode String) => String`: URI-encode input text, making the output suitable to be included as part of a link or other URL. + +### Writing a Handlebars Theme + +Handlebars themes should have, roughly, the skeleton below: ```html @@ -164,8 +201,10 @@ Themes should have, roughly, the structure below: {{#if font.css}} {{{font.css}}} {{/if}} - {{#if (or font.name font.css)}} - * { font-family: '{{#if font.name}}{{font.name}}{{/if}}{{#if font.url}}included_font{{/if}}' } + /* cursed hack to deal with weird rendering in chrome */ + * { font-size: {{#if font}}17px{{else}}{{#if font.scale}}calc(20px * {{font.scale}}){{else}}20px{{/if}}{{/if}}; } + {{#if font.name }} + * { font-family: '{{font.name}}' } {{/if}} /* actual styles */ @@ -178,7 +217,83 @@ Themes should have, roughly, the structure below: ``` -You will be passed one of 2 context objects, depending on if there was an error processing the request. +See below for the [Context Object](#the-context-object) + +## Lua Themes + +Lua is a powerful scripting language, for themes that need to do a lot of math on the server. +`lfm_embed` uses the [Luau](https://luau-lang.org/) implementation, specicially. + +You are provided with only the `table`, `string`, `utf8`, `bit`, and `math` stdlib modules. +You are also provided with the `html` global. + +- `html(el: String, tbl: Table) => String`: Generates the HTML for ``, with the sequential entries in `tbl` as concatenated text, and the named entries as attributes. Sequential entries wrapped in a table will be escaped. + +- `html.(tbl: Table) => String` Functions like `html`, but with the index name as the element type. It is intended to be called like `html.a{"Homepage", href = "/"}`. + +- `html.root(tbl: Table) => String` Generates the root of an HTML document, including doctype, processing `tbl` as normal. + +Example: + +```lua +print(html.root{html.body{html.p{"Check out my cool site!"}}}) +-- +--

Check out my cool site!

+ +print(html.p{ "This doesn't get escaped, ", {"but this does!"} }) +--

This doesn't get escaped, but this <b>does</b>!

+``` + +### Writing a Lua Theme + +Lua theme scripts are expected to return a function which takes one argument, the [Context Object](#the-context-object), and return a fully formed html document. +They may contain anything as far as locals go, but are not allowed to manipulate the global context. +The returned function will be repeatedly called for any incoming request. + +Lua themes should have, roughly, the skeleton below: + +```lua +local stylefmt = [[ +/* font css */ +%s +* { font-size: %spx; } +%s +/* actual styles go here*/ +]] + +return function(ctx) + local contents + local style + if ctx.error then + contents = html.p{{ctx.error}} + style = "" + else + contents = "" + if not ctx.font then ctx.font = {} end + local font_setter = "" + if ctx.font.name then + font_setter = string.format("* { font-family: '%s'; }", ctx.font.name) + end + local font_px + -- hack to deal with chrome being weird + if ctx.font then font_px = 17 + elseif ctx.font.scale then font_px = 20 * ctx.font.scale + else font_px = 20 end + style = string.format(stylefmt, ctx.font.css or "", font_px, font_setter) + end + return html.root{lang = "en", + html.head{ + html.meta{charset = "UTF-8"}, + html.style{style} + }, + html.body{contents} + } +end +``` + +## The Context Object + +You will be passed one of 2 context objects, depending on if there was an error dealing with the remote backends. If there was an error, the object will just be `{ error: String }`, otherwise it will be the following: ```js @@ -193,7 +308,7 @@ If there was an error, the object will just be `{ error: String }`, otherwise it image_url: String, // Link to user's profile. url: String, - + // Total scrobbles. scrobble_count: Number, // Number of artists in library. @@ -202,11 +317,11 @@ If there was an error, the object will just be `{ error: String }`, otherwise it track_count: Number, // Number of albums in library. album_count: Number, - + // True if user subscribes to last.fm pro. pro_subscriber: Boolean }, - + // The user's most current, or most scrobble. scrobble: { // The name of the track. @@ -228,19 +343,21 @@ If there was an error, the object will just be `{ error: String }`, otherwise it url: String, // True if the user has loved the track. - loved: Boolean + loved: Boolean, // True if the user is currently scrobbling it, false if it's just // the most recently played track. - now_playing: Boolean, + now_playing: Boolean }, // Custom font info. font: { // Set if the theme should replace the custom font with something. - name: String? + name: String?, // Will contain CSS which includes a custom font as 'included_font'. - css: String? - } + css: String?, + // Will contain a factor of the base font scale to scale the theme's font by, for custom font reasons. + scale: Number? + }, // A set of extraneous query parameters. // @@ -252,28 +369,6 @@ If there was an error, the object will just be `{ error: String }`, otherwise it } ``` -In addition, the following [helpers](https://handlebarsjs.com/guide/expressions.html#helpers) are provided, on top of the handlebars [builtins](https://handlebarsjs.com/guide/builtin-helpers.html): - -- `(eq Object Object) => Boolean`: Check equality between its args. - -- `(ne Object Object) => Boolean`: Check inequality between its args. - -- `(gt Number|String Number|String) => Boolean`: Check if the first arg is greater than the second. - -- `(gte Number|String Number|String) => Boolean`: Check if the first arg is greater than or equal to the second. - -- `(lt Number|String Number|String) => Boolean`: Check if the first arg is less than the second. - -- `(lte Number|String Number|String) => Boolean`: Check if the first arg is less than or equal to the second. - -- `(and Boolean Boolean) => Boolean`: Boolean AND gate. - -- `(or Boolean Boolean) => Boolean`: Boolean OR gate. - -- `(not Boolean) => Boolean`: Boolean NOT gate. - -- `(uri_encode String) => String`: URI-encode input text, making the output suitable to be included as part of a link or other URL. - # Contributing [E-Mail me](mailto:alyx@aleteoryx.me) if you'd like to help out, submit a custom theme, or request a feature. diff --git a/src/theming/hbs/plain.hbs b/src/theming/hbs/plain.hbs index 975d6c5..b3c0abf 100644 --- a/src/theming/hbs/plain.hbs +++ b/src/theming/hbs/plain.hbs @@ -13,8 +13,8 @@ a { color: cyan; } {{/if}} * { font-size: {{#if font}}17px{{else}}{{#if font.scale}}calc(20px * {{font.scale}}){{else}}20px{{/if}}{{/if}}; } - {{#if (or font.name font.css)}} - * { font-family: '{{#if font.name}}{{font.name}}{{/if}}' } + {{#if font.name}} + * { font-family: '{{font.name}}' } {{/if}} p { margin: 0px; padding: 0px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } diff --git a/src/theming/lua.rs b/src/theming/lua.rs index 0699f09..118762b 100644 --- a/src/theming/lua.rs +++ b/src/theming/lua.rs @@ -4,7 +4,7 @@ use std::path::Path; use std::fs; use std::sync::Mutex; -use mlua::{Lua, LuaSerdeExt, Compiler, StdLib, Value, LuaOptions, Table}; +use mlua::{Lua, LuaSerdeExt, Compiler, StdLib, Value, LuaOptions, Table, SerializeOptions}; use http::StatusCode; use crate::CONFIG; @@ -115,7 +115,12 @@ pub fn render_theme(name: &str, ctx: &crate::ctx::Ctx) -> Option { log::error!("Error loading `{name}`: {e}"); return Some(Err(StatusCode::INTERNAL_SERVER_ERROR)); }, //TODO: gate behind flag } - let ctx = match lua.to_value(ctx) { + let opts = SerializeOptions::new() + .serialize_none_to_null(false) + .serialize_unit_to_null(false) + .set_array_metatable(false); + + let ctx = match lua.to_value_with(ctx, opts) { Ok(ok) => ok, Err(e) => { log::error!("Lua context serialization error: {e}"); -- 2.45.2