1use std::sync::atomic::{AtomicUsize, Ordering};
2
3use sofie_core::{Show, Shown};
4use sofie_values::FormField;
5
6use crate::catalog::{self, Submitted};
7use crate::error::{RenderError, TemplateError};
8use crate::language::Language;
9use crate::page::CLEARED_NOTICE;
10use crate::render;
11use crate::syntax::{LocatedError, Op};
12
13const MAXIMUM_CAPACITY_HINT: usize = 256 * 1024;
15
16#[derive(Debug)]
17pub struct Template {
18 pub(crate) name: String,
19 pub(crate) ops: Vec<Op>,
20 document_capacity: AtomicUsize,
22 fragment_capacity: AtomicUsize,
23 pub(crate) precomputed: Precomputed,
24}
25
26#[derive(Debug)]
28pub(crate) struct Precomputed {
29 #[cfg(feature = "clients")]
30 pub(crate) client_parts: std::sync::OnceLock<crate::client_view::ClientParts>,
31 #[cfg(feature = "gates")]
32 pub(crate) gated_paths: crate::rendering::GatedPaths,
33}
34
35impl Precomputed {
36 #[cfg_attr(
37 not(feature = "gates"),
38 expect(unused_variables, reason = "only gates are read from the ops up front")
39 )]
40 fn of(ops: &[Op]) -> Self {
41 Self {
42 #[cfg(feature = "clients")]
43 client_parts: std::sync::OnceLock::new(),
44 #[cfg(feature = "gates")]
45 gated_paths: crate::rendering::GatedPaths::of(ops),
46 }
47 }
48
49 #[cfg_attr(
51 not(feature = "clients"),
52 expect(clippy::unused_self, reason = "only a client view describes forms")
53 )]
54 fn forget_forms(&mut self) {
55 #[cfg(feature = "clients")]
56 self.client_parts.take();
57 }
58}
59
60impl Template {
61 pub(crate) fn new(name: &str, ops: Vec<Op>) -> Self {
62 Self {
63 precomputed: Precomputed::of(&ops),
64 name: name.to_owned(),
65 ops,
66 document_capacity: AtomicUsize::new(0),
67 fragment_capacity: AtomicUsize::new(0),
68 }
69 }
70
71 #[cfg(feature = "parse")]
72 pub fn parse(name: &str, source: &str) -> Result<Self, TemplateError> {
73 Self::parse_with(name, source, &crate::Components::default())
74 }
75
76 #[cfg(feature = "parse")]
77 pub fn parse_with(
78 name: &str,
79 source: &str,
80 components: &crate::Components,
81 ) -> Result<Self, TemplateError> {
82 crate::compiled::parse_template(name, source, components)
83 }
84
85 pub fn describe_form(
87 &mut self,
88 form_id: &str,
89 fields: &'static [FormField],
90 ) -> Result<(), TemplateError> {
91 self.precomputed.forget_forms();
92 match catalog::describe_forms(&mut self.ops, form_id, fields) {
93 Ok(true) => Ok(()),
94 Ok(false) => Err(TemplateError::unlocated(
95 &self.name,
96 format!("there is no `Form` with id `{form_id}` to describe"),
97 )),
98 Err(error) => Err(self.defect(error)),
99 }
100 }
101
102 #[must_use]
103 pub fn name(&self) -> &str {
104 &self.name
105 }
106
107 pub fn render(&self, data: &(impl Show + ?Sized)) -> Result<String, TemplateError> {
108 render_whole(self, data.show(), None, Language::plain(), false)
109 }
110
111 pub fn render_submitted(
112 &self,
113 data: &(impl Show + ?Sized),
114 submitted: &Submitted<'_>,
115 ) -> Result<String, TemplateError> {
116 render_whole(self, data.show(), Some(submitted), Language::plain(), false)
117 }
118
119 pub fn render_fragment(
120 &self,
121 data: &(impl Show + ?Sized),
122 id: &str,
123 ) -> Result<String, RenderError> {
124 render_part(self, data.show(), id, Language::plain(), false)
125 }
126
127 pub fn render_out_of_band(
129 &self,
130 data: &(impl Show + ?Sized),
131 id: &str,
132 ) -> Result<String, RenderError> {
133 self.render_fragment(data, id).map(mark_out_of_band)
134 }
135
136 fn defect(&self, error: LocatedError) -> TemplateError {
137 TemplateError::located(&self.name, error)
138 }
139}
140
141#[must_use]
144pub fn form_fields<'t>(template: &'t Template, form_id: &str) -> Option<Vec<&'t str>> {
145 let mut names = None;
146 catalog::form_fields(&template.ops, form_id, &mut names);
147 names
148}
149
150pub(crate) fn render_whole<'a>(
151 template: &'a Template,
152 data: Shown<'a>,
153 submitted: Option<&'a Submitted<'a>>,
154 language: Language<'a>,
155 keyed_forms: bool,
156) -> Result<String, TemplateError> {
157 let capacity = template.document_capacity.load(Ordering::Relaxed);
158 let html = render::render_document(
159 &template.ops,
160 data,
161 capacity,
162 submitted,
163 language,
164 keyed_forms,
165 )
166 .map_err(|error| template.defect(error))?;
167 template
168 .document_capacity
169 .store(html.len().min(MAXIMUM_CAPACITY_HINT), Ordering::Relaxed);
170 Ok(html)
171}
172
173pub(crate) fn render_part<'a>(
174 template: &'a Template,
175 data: Shown<'a>,
176 id: &'a str,
177 language: Language<'a>,
178 keyed_forms: bool,
179) -> Result<String, RenderError> {
180 let capacity = template
182 .fragment_capacity
183 .load(Ordering::Relaxed)
184 .saturating_add(CLEARED_NOTICE.len());
185 let html = render::render_fragment(&template.ops, data, id, capacity, language, keyed_forms)
186 .map_err(|error| RenderError::Template(template.defect(error)))?
187 .ok_or_else(|| RenderError::FragmentNotFound { id: id.to_owned() })?;
188 template
189 .fragment_capacity
190 .store(html.len().min(MAXIMUM_CAPACITY_HINT), Ordering::Relaxed);
191 Ok(html)
192}
193
194pub(crate) fn mark_out_of_band(mut html: String) -> String {
195 if let Some(name_length) = html
196 .strip_prefix('<')
197 .and_then(|rest| rest.find([' ', '>']))
198 {
199 html.insert_str(1 + name_length, " hx-swap-oob=\"true\"");
200 }
201 html
202}
203
204#[cfg(all(test, feature = "parse"))]
205mod tests {
206 use std::sync::atomic::Ordering;
207
208 use serde_json::json;
209
210 use super::{MAXIMUM_CAPACITY_HINT, Template};
211
212 #[test]
213 fn one_huge_render_does_not_make_every_later_one_reserve_as_much() {
214 let template = Template::parse("rows", "<p>{data.text}</p>").unwrap();
215
216 template
217 .render(&json!({"text": "x".repeat(4 * MAXIMUM_CAPACITY_HINT)}))
218 .unwrap();
219
220 assert_eq!(
221 template.document_capacity.load(Ordering::Relaxed),
222 MAXIMUM_CAPACITY_HINT
223 );
224 }
225}