sofie_engine/client/
mod.rs1mod emit;
4mod script;
5mod tree;
6
7use std::fmt;
8
9use crate::{Components, Template, TemplateError};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Client {
13 Svelte,
14}
15
16impl Client {
17 #[must_use]
19 pub fn name(self) -> &'static str {
20 match self {
21 Self::Svelte => "svelte",
22 }
23 }
24
25 #[must_use]
26 pub fn extension(self) -> &'static str {
27 match self {
28 Self::Svelte => "svelte",
29 }
30 }
31}
32
33impl fmt::Display for Client {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 formatter.write_str(match self {
36 Self::Svelte => "Svelte",
37 })
38 }
39}
40
41pub fn compile_client(
43 name: &str,
44 source: &str,
45 components: &Components,
46 client: Client,
47) -> Result<String, TemplateError> {
48 let template = Template::parse_with(name, source, components)?;
49 tree::tree(&template.ops)
50 .and_then(|nodes| emit::Emitter::new().screen(&nodes))
51 .map_err(|message| {
52 TemplateError::unlocated(name, format!("compiling for {client}: {message}"))
53 })
54}
55
56#[cfg(test)]
57mod tests {
58 use super::{Client, compile_client};
59 use crate::Components;
60
61 const SOURCE: &str = "<Section title=\"Tom & Jerry\">\
62 <ul class=\"list {data.kind}\">{#each data.items as item, index}\
63 <li><a href={item.url}>{item.name}</a></li>{/each}</ul>\
64 {#if data.empty}<p>None {\"{x}\"}</p>{:else}<input required>{/if}\
65 <Table id=\"t\" of={data.rows} columns=\"name\" as=\"row\">\
66 <Action post=\"/r/{row.id}\" label=\"Go\" /></Table></Section>";
67
68 fn compiled(client: Client) -> String {
69 compile_client("screen", SOURCE, &Components::default(), client).unwrap()
70 }
71
72 #[test]
73 fn svelte_keeps_markup_and_writes_blocks_and_snippets() {
74 let svelte = compiled(Client::Svelte);
75
76 for expected in [
77 "props={{\"title\": S.text(\"Tom & Jerry\")}}",
78 "<ul class=\"list {S.text(data?.kind)}\">",
79 "{#each S.items(data?.items) as s0, s1}<li><a href=\"{S.url(S.text(s0?.url))}\">",
80 "{S.text(s0?.name)}</a></li>{/each}",
81 "{#if S.truthy(data?.empty)}<p>None {S.text(\"{x}\")}</p>{:else}<input required=\"\">{/if}",
82 "{#snippet row(s0)}<Catalog {view} name=\"Action\" props={{\"post\": \
83 S.join([\"/r/\", S.urlPart(s0?.id)]), \"label\": S.text(\"Go\")}} />{/snippet}",
84 ] {
85 assert!(svelte.contains(expected), "{expected}\n{svelte}");
86 }
87 }
88
89 #[test]
90 fn an_action_passes_its_posted_fields_as_values() {
91 let source = "{#each data.slots as slot}<Action post=\"/slots/book\" label=\"Book\" \
92 with={{slot: slot.id, seats: 2}} />{/each}";
93
94 let svelte =
95 compile_client("slots", source, &Components::default(), Client::Svelte).unwrap();
96
97 assert!(
98 svelte.contains("\"with\": {\"slot\": s0?.id, \"seats\": 2}"),
99 "{svelte}"
100 );
101 }
102
103 #[test]
104 fn a_second_form_sharing_an_id_is_looked_up_under_its_own_key() {
105 let source = "{#if data.a}<Form id=\"f\" post=\"/f\"><Submit label=\"Go\" /></Form>\
106 {:else}<Form id=\"f\" post=\"/f\"><Submit label=\"Later\" /></Form>{/if}";
107
108 let svelte =
109 compile_client("forms", source, &Components::default(), Client::Svelte).unwrap();
110
111 assert_eq!(
112 svelte.matches("\"describedAs\": \"f#2\"").count(),
113 1,
114 "{svelte}"
115 );
116 }
117
118 #[test]
119 fn a_svelte_screen_s_title_names_the_page_through_its_head() {
120 let svelte = compile_client(
121 "order",
122 "<title>Order {data.id}</title><p>x</p>",
123 &Components::default(),
124 Client::Svelte,
125 )
126 .unwrap();
127
128 assert!(
129 svelte.contains(
130 "<svelte:head><title>Order {S.text(data?.id)}</title></svelte:head><p>x</p>"
131 ),
132 "{svelte}"
133 );
134 }
135
136 #[test]
137 fn written_pages_stay_in_html_areas() {
138 let error = compile_client(
139 "page",
140 "<Markdown text={data.body} />",
141 &Components::default(),
142 Client::Svelte,
143 )
144 .unwrap_err();
145
146 assert!(error.message.contains("only in HTML areas"), "{error}");
147 }
148
149 #[test]
150 fn page_script_attributes_are_kept_for_the_client_runtime() {
151 let svelte = compile_client(
152 "passkeys",
153 "<button type=\"button\" data-sofie-passkey=\"create\" data-options={data.options} \
154 data-post=\"passkeys\">Add</button><form method=\"post\" action={data.action} \
155 data-sofie-autosubmit=\"true\"></form>",
156 &Components::default(),
157 Client::Svelte,
158 )
159 .unwrap();
160
161 for expected in [
162 "<button type=\"button\" data-sofie-passkey=\"create\" \
163 data-options=\"{S.text(data?.options)}\" data-post=\"passkeys\">Add</button>",
164 "data-sofie-autosubmit=\"true\"",
165 ] {
166 assert!(svelte.contains(expected), "{expected}\n{svelte}");
167 }
168 }
169}