mod emit;
mod script;
mod tree;
use std::fmt;
use crate::{Components, Template, TemplateError};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Client {
Svelte,
}
impl Client {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Svelte => "svelte",
}
}
#[must_use]
pub fn extension(self) -> &'static str {
match self {
Self::Svelte => "svelte",
}
}
}
impl fmt::Display for Client {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Svelte => "Svelte",
})
}
}
pub fn compile_client(
name: &str,
source: &str,
components: &Components,
client: Client,
) -> Result<String, TemplateError> {
let template = Template::parse_with(name, source, components)?;
tree::tree(&template.ops)
.and_then(|nodes| emit::Emitter::new().screen(&nodes))
.map_err(|message| {
TemplateError::unlocated(name, format!("compiling for {client}: {message}"))
})
}
#[cfg(test)]
mod tests {
use super::{Client, compile_client};
use crate::Components;
const SOURCE: &str = "<Section title=\"Tom & Jerry\">\
<ul class=\"list {data.kind}\">{#each data.items as item, index}\
<li><a href={item.url}>{item.name}</a></li>{/each}</ul>\
{#if data.empty}<p>None {\"{x}\"}</p>{:else}<input required>{/if}\
<Table id=\"t\" of={data.rows} columns=\"name\" as=\"row\">\
<Action post=\"/r/{row.id}\" label=\"Go\" /></Table></Section>";
fn compiled(client: Client) -> String {
compile_client("screen", SOURCE, &Components::default(), client).unwrap()
}
#[test]
fn svelte_keeps_markup_and_writes_blocks_and_snippets() {
let svelte = compiled(Client::Svelte);
for expected in [
"props={{\"title\": S.text(\"Tom & Jerry\")}}",
"<ul class=\"list {S.text(data?.kind)}\">",
"{#each S.items(data?.items) as s0, s1}<li><a href=\"{S.url(S.text(s0?.url))}\">",
"{S.text(s0?.name)}</a></li>{/each}",
"{#if S.truthy(data?.empty)}<p>None {S.text(\"{x}\")}</p>{:else}<input required=\"\">{/if}",
"{#snippet row(s0)}<Catalog {view} name=\"Action\" props={{\"post\": \
S.join([\"/r/\", S.urlPart(s0?.id)]), \"label\": S.text(\"Go\")}} />{/snippet}",
] {
assert!(svelte.contains(expected), "{expected}\n{svelte}");
}
}
#[test]
fn an_action_passes_its_posted_fields_as_values() {
let source = "{#each data.slots as slot}<Action post=\"/slots/book\" label=\"Book\" \
with={{slot: slot.id, seats: 2}} />{/each}";
let svelte =
compile_client("slots", source, &Components::default(), Client::Svelte).unwrap();
assert!(
svelte.contains("\"with\": {\"slot\": s0?.id, \"seats\": 2}"),
"{svelte}"
);
}
#[test]
fn a_second_form_sharing_an_id_is_looked_up_under_its_own_key() {
let source = "{#if data.a}<Form id=\"f\" post=\"/f\"><Submit label=\"Go\" /></Form>\
{:else}<Form id=\"f\" post=\"/f\"><Submit label=\"Later\" /></Form>{/if}";
let svelte =
compile_client("forms", source, &Components::default(), Client::Svelte).unwrap();
assert_eq!(
svelte.matches("\"describedAs\": \"f#2\"").count(),
1,
"{svelte}"
);
}
#[test]
fn a_svelte_screen_s_title_names_the_page_through_its_head() {
let svelte = compile_client(
"order",
"<title>Order {data.id}</title><p>x</p>",
&Components::default(),
Client::Svelte,
)
.unwrap();
assert!(
svelte.contains(
"<svelte:head><title>Order {S.text(data?.id)}</title></svelte:head><p>x</p>"
),
"{svelte}"
);
}
#[test]
fn written_pages_stay_in_html_areas() {
let error = compile_client(
"page",
"<Markdown text={data.body} />",
&Components::default(),
Client::Svelte,
)
.unwrap_err();
assert!(error.message.contains("only in HTML areas"), "{error}");
}
#[test]
fn page_script_attributes_are_kept_for_the_client_runtime() {
let svelte = compile_client(
"passkeys",
"<button type=\"button\" data-sofie-passkey=\"create\" data-options={data.options} \
data-post=\"passkeys\">Add</button><form method=\"post\" action={data.action} \
data-sofie-autosubmit=\"true\"></form>",
&Components::default(),
Client::Svelte,
)
.unwrap();
for expected in [
"<button type=\"button\" data-sofie-passkey=\"create\" \
data-options=\"{S.text(data?.options)}\" data-post=\"passkeys\">Add</button>",
"data-sofie-autosubmit=\"true\"",
] {
assert!(svelte.contains(expected), "{expected}\n{svelte}");
}
}
}