Skip to main content

sofie_engine/compiled/
mod.rs

1//! A template's program as bytes: the header `sft` and the format version as
2//! two little-endian bytes, then the ops. Lengths, counts and positions are
3//! LEB128 integers, text is a length and UTF-8, an integer literal eight
4//! little-endian bytes, and every op and expression starts with its tag.
5//! Components are kept as written, by name and props, so the bytes do not
6//! depend on how the catalog stores them.
7
8mod read;
9#[cfg(feature = "parse")]
10mod write;
11
12#[cfg(feature = "parse")]
13use std::collections::HashMap;
14
15use self::read::read;
16#[cfg(feature = "parse")]
17use self::write::write;
18use crate::catalog::build_all;
19#[cfg(feature = "parse")]
20use crate::components::Components;
21#[cfg(feature = "parse")]
22use crate::parse::each_call;
23use crate::syntax::{Op, Written};
24use crate::{Template, TemplateError};
25
26/// Raised whenever the bytes for the same template would change.
27const FORMAT: u16 = 2;
28const MAGIC: &[u8; 3] = b"sft";
29
30mod tag {
31    pub(super) const MARKUP: u8 = 0;
32    pub(super) const TEXT: u8 = 1;
33    pub(super) const URL: u8 = 2;
34    pub(super) const IDENTIFIED: u8 = 3;
35    pub(super) const COMPONENT: u8 = 4;
36    pub(super) const IF: u8 = 5;
37    pub(super) const EACH: u8 = 6;
38
39    pub(super) const PATH: u8 = 0;
40    pub(super) const QUOTED: u8 = 1;
41    pub(super) const INTEGER: u8 = 2;
42    pub(super) const AUTHORED: u8 = 3;
43    pub(super) const INTERPOLATED: u8 = 4;
44    pub(super) const TRANSLATE: u8 = 5;
45    pub(super) const FORMAT_CALL: u8 = 6;
46    pub(super) const LABEL: u8 = 7;
47    pub(super) const GATE: u8 = 8;
48
49    pub(super) const DATE_FORMAT: u8 = 2;
50}
51
52/// Reads what [`compile_template`] wrote, so a built app needs no parser.
53pub fn load_template(name: &str, compiled: &[u8]) -> Result<Template, TemplateError> {
54    let written = read(compiled).map_err(|message| TemplateError::unlocated(name, message))?;
55    build(name, written)
56}
57
58#[cfg(feature = "parse")]
59pub fn compile_template(name: &str, source: &str) -> Result<Vec<u8>, TemplateError> {
60    compile_template_with(name, source, &Components::default())
61}
62
63/// The module components a screen writes are compiled into it.
64#[cfg(feature = "parse")]
65pub fn compile_template_with(
66    name: &str,
67    source: &str,
68    components: &Components,
69) -> Result<Vec<u8>, TemplateError> {
70    let written = parse_written(name, source, components)?;
71    let compiled = write(&written);
72    checked(name, written)?;
73    Ok(compiled)
74}
75
76/// Each `t(…)` call keeps its text in the source language of `translations`, so an app built
77/// without `i18n` writes it.
78#[cfg(all(feature = "parse", feature = "i18n"))]
79pub fn compile_template_with_texts(
80    name: &str,
81    source: &str,
82    components: &Components,
83    translations: &crate::Translations,
84) -> Result<Vec<u8>, TemplateError> {
85    let mut written = parse_written(name, source, components)?;
86    crate::translations::record(&mut written, translations.negotiate(None, None));
87    let compiled = write(&written);
88    checked(name, written)?;
89    Ok(compiled)
90}
91
92#[cfg(feature = "parse")]
93pub(crate) fn parse_template(
94    name: &str,
95    source: &str,
96    components: &Components,
97) -> Result<Template, TemplateError> {
98    checked(name, parse_written(name, source, components)?)
99}
100
101/// As `Template::parse_with`, with each `t(…)` given the source-language text the screen's
102/// `compiled` program recorded for its key: a development build rereads a screen this way, and a
103/// build without `i18n` writes that text.
104#[cfg(feature = "parse")]
105pub fn parse_template_with_recorded(
106    name: &str,
107    source: &str,
108    components: &Components,
109    compiled: &[u8],
110) -> Result<Template, TemplateError> {
111    let mut program = read(compiled).map_err(|message| TemplateError::unlocated(name, message))?;
112    let mut texts = HashMap::new();
113    each_call(&mut program, &mut |call| {
114        if let Some((key, recorded)) = call.recorded_mut()
115            && let Some(text) = recorded.take()
116        {
117            texts.insert(key.to_owned(), text);
118        }
119    });
120    let mut written = parse_written(name, source, components)?;
121    each_call(&mut written, &mut |call| {
122        if let Some((key, recorded)) = call.recorded_mut() {
123            *recorded = texts.get(key).cloned();
124        }
125    });
126    checked(name, written)
127}
128
129#[cfg(feature = "parse")]
130fn parse_written(
131    name: &str,
132    source: &str,
133    components: &Components,
134) -> Result<Vec<Op<Written>>, TemplateError> {
135    crate::parse::parse(source, components).map_err(|error| TemplateError::located(name, error))
136}
137
138#[cfg(feature = "parse")]
139fn checked(name: &str, written: Vec<Op<Written>>) -> Result<Template, TemplateError> {
140    let template = build(name, written)?;
141    #[cfg(feature = "i18n")]
142    crate::translations::key_uses(&template.ops)
143        .map_err(|error| TemplateError::located(name, error))?;
144    Ok(template)
145}
146
147pub(crate) fn build(name: &str, written: Vec<Op<Written>>) -> Result<Template, TemplateError> {
148    let ops = build_all(written).map_err(|error| TemplateError::located(name, error))?;
149    Ok(Template::new(name, ops))
150}
151
152#[cfg(all(test, feature = "parse", not(feature = "i18n")))]
153mod tests {
154    use serde_json::json;
155
156    use super::{load_template, parse_template_with_recorded, write};
157    use crate::Components;
158    use crate::language::{Argument, Call};
159    use crate::message::{Message, Recorded};
160    use crate::syntax::{Expression, Op, Path, Position, Root};
161
162    fn recorded_call(key: &str, recorded: Option<Recorded>) -> Op<crate::syntax::Written> {
163        let count = Argument {
164            name: "count".to_owned(),
165            value: Expression::Path(Path {
166                root: Root::Data,
167                fields: vec!["count".to_owned()],
168            }),
169        };
170        let position = Position { line: 1, column: 1 };
171        let call = Call::translate(key.to_owned(), vec![count], position, recorded);
172        Op::Text(Expression::Call(Box::new(call)))
173    }
174
175    #[test]
176    fn a_build_without_i18n_writes_the_recorded_text_with_its_arguments() {
177        let recorded = Recorded {
178            other: Message::parse("{count} <orders>").unwrap(),
179            one: Some(Message::parse("one order").unwrap()),
180        };
181        let compiled = write(&[recorded_call("orders.shown", Some(recorded))]);
182        let loaded = load_template("orders", &compiled).unwrap();
183
184        assert_eq!(
185            loaded.render(&json!({"count": 2})).unwrap(),
186            "2 &lt;orders&gt;"
187        );
188        assert_eq!(loaded.render(&json!({"count": 1})).unwrap(), "one order");
189    }
190
191    #[test]
192    fn a_call_compiled_without_its_text_writes_its_key() {
193        let compiled = write(&[recorded_call("orders.shown", None)]);
194        let loaded = load_template("orders", &compiled).unwrap();
195
196        assert_eq!(loaded.render(&json!({"count": 2})).unwrap(), "orders.shown");
197    }
198
199    #[test]
200    fn a_screen_parsed_again_without_i18n_writes_the_texts_its_compiled_program_recorded() {
201        let recorded = Recorded {
202            other: Message::parse("{count} orders").unwrap(),
203            one: None,
204        };
205        let compiled = write(&[recorded_call("orders.shown", Some(recorded))]);
206        let source = "<p>{t(\"orders.shown\", count = data.count)}, {t(\"orders.new\")}</p>";
207
208        let parsed =
209            parse_template_with_recorded("orders", source, &Components::default(), &compiled)
210                .unwrap();
211
212        assert_eq!(
213            parsed.render(&json!({"count": 2})).unwrap(),
214            "<p>2 orders, orders.new</p>"
215        );
216    }
217}