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())
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())
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())
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) -> Result<String, TemplateError> {
156 let capacity = template.document_capacity.load(Ordering::Relaxed);
157 let html = render::render_document(&template.ops, data, capacity, submitted, language)
158 .map_err(|error| template.defect(error))?;
159 template
160 .document_capacity
161 .store(html.len().min(MAXIMUM_CAPACITY_HINT), Ordering::Relaxed);
162 Ok(html)
163}
164
165pub(crate) fn render_part<'a>(
166 template: &'a Template,
167 data: Shown<'a>,
168 id: &'a str,
169 language: Language<'a>,
170) -> Result<String, RenderError> {
171 let capacity = template
173 .fragment_capacity
174 .load(Ordering::Relaxed)
175 .saturating_add(CLEARED_NOTICE.len());
176 let html = render::render_fragment(&template.ops, data, id, capacity, language)
177 .map_err(|error| RenderError::Template(template.defect(error)))?
178 .ok_or_else(|| RenderError::FragmentNotFound { id: id.to_owned() })?;
179 template
180 .fragment_capacity
181 .store(html.len().min(MAXIMUM_CAPACITY_HINT), Ordering::Relaxed);
182 Ok(html)
183}
184
185pub(crate) fn mark_out_of_band(mut html: String) -> String {
186 if let Some(name_length) = html
187 .strip_prefix('<')
188 .and_then(|rest| rest.find([' ', '>']))
189 {
190 html.insert_str(1 + name_length, " hx-swap-oob=\"true\"");
191 }
192 html
193}
194
195#[cfg(all(test, feature = "parse"))]
196mod tests {
197 use std::sync::atomic::Ordering;
198
199 use serde_json::json;
200
201 use super::{MAXIMUM_CAPACITY_HINT, Template};
202
203 #[test]
204 fn one_huge_render_does_not_make_every_later_one_reserve_as_much() {
205 let template = Template::parse("rows", "<p>{data.text}</p>").unwrap();
206
207 template
208 .render(&json!({"text": "x".repeat(4 * MAXIMUM_CAPACITY_HINT)}))
209 .unwrap();
210
211 assert_eq!(
212 template.document_capacity.load(Ordering::Relaxed),
213 MAXIMUM_CAPACITY_HINT
214 );
215 }
216}