Skip to main content

sofie_engine/translations/
uses.rs

1use super::Call;
2#[cfg(feature = "content")]
3use crate::catalog::content::Published;
4#[cfg(feature = "layout")]
5use crate::catalog::layout::Layout;
6use crate::catalog::{Component, Content, FormPart};
7use crate::syntax::{Expression, LocatedError, Op, Position};
8
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct KeyUse {
11    pub key: String,
12    pub kind: KeyKind,
13    pub line: usize,
14    pub column: usize,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum KeyKind {
19    Message { arguments: Vec<String> },
20    Label,
21    Enum,
22}
23
24/// Calls write text, so one where a value, a URL or an id is read is refused.
25pub(crate) fn key_uses(ops: &[Op]) -> Result<Vec<KeyUse>, LocatedError> {
26    let mut walker = Walker { uses: Vec::new() };
27    walker.ops(ops)?;
28    Ok(walker.uses)
29}
30
31struct Walker {
32    uses: Vec<KeyUse>,
33}
34
35impl Walker {
36    fn ops(&mut self, ops: &[Op]) -> Result<(), LocatedError> {
37        for op in ops {
38            match op {
39                Op::Markup(_) => {}
40                Op::Text(text) => self.text(text),
41                Op::Url(url) => refuse_call(url)?,
42                Op::Identified { id, body } => {
43                    refuse_call(id)?;
44                    self.ops(body)?;
45                }
46                Op::Component(component) => self.component(component)?,
47                Op::If(conditional) => {
48                    refuse_call(&conditional.condition)?;
49                    self.ops(&conditional.then_branch)?;
50                    self.ops(&conditional.else_branch)?;
51                }
52                Op::Each(each) => {
53                    refuse_call(&each.list)?;
54                    self.ops(&each.body)?;
55                }
56            }
57        }
58        Ok(())
59    }
60
61    fn component(&mut self, component: &Component) -> Result<(), LocatedError> {
62        refuse_calls([component.id.as_ref()])?;
63        match &component.content {
64            Content::Section(section) => {
65                self.text(&section.title);
66                self.ops(&section.children)
67            }
68            Content::Table(table) => {
69                refuse_calls([table.link.as_ref()])?;
70                self.ops(&table.header)?;
71                self.ops(&table.row_content)
72            }
73            Content::Money(_) => Ok(()),
74            Content::Action(action) => {
75                self.text(&action.label);
76                refuse_calls([
77                    Some(&action.post),
78                    action.when.as_ref(),
79                    action.target.as_ref().map(|target| &target.id),
80                ])
81            }
82            Content::Form(form) => {
83                refuse_calls([
84                    Some(&form.post),
85                    form.target.as_ref().map(|target| &target.id),
86                ])?;
87                form.parts.iter().try_for_each(|part| self.form_part(part))
88            }
89            Content::FormPart(part) => self.form_part(part),
90            #[cfg(feature = "layout")]
91            Content::Layout(Layout::Grid(children)) => self.ops(children),
92            #[cfg(feature = "layout")]
93            Content::Layout(Layout::Card { href, children }) => {
94                refuse_calls([href.as_ref()])?;
95                self.ops(children)
96            }
97            #[cfg(feature = "layout")]
98            Content::Layout(Layout::Image { src, alt }) => {
99                self.text(alt);
100                refuse_call(src)
101            }
102            Content::Badge(badge) => {
103                self.enumeration(&badge.enumeration, badge.position);
104                Ok(())
105            }
106            #[cfg(feature = "islands")]
107            Content::Island(island) => refuse_call(&island.props),
108            #[cfg(feature = "content")]
109            Content::Published(Published::Markdown(text)) => refuse_call(text),
110            #[cfg(feature = "content")]
111            Content::Published(Published::Head(head)) => {
112                for text in [&head.title, &head.description, &head.kind]
113                    .into_iter()
114                    .flatten()
115                {
116                    self.text(text);
117                }
118                refuse_calls([head.image.as_ref(), head.url.as_ref()])
119            }
120        }
121    }
122
123    fn form_part(&mut self, part: &FormPart) -> Result<(), LocatedError> {
124        let field = match part {
125            FormPart::Submit(submit) => {
126                self.text(&submit.label);
127                return Ok(());
128            }
129            FormPart::Field(field) => field,
130        };
131        self.text(&field.label);
132        let [value, options, currency] = [&field.value, &field.options, &field.currency]
133            .map(|attribute| attribute.as_ref().map(|attribute| &attribute.value));
134        if let Some(Expression::Authored(name)) = options {
135            self.enumeration(name, field.position);
136        }
137        refuse_calls([value, options, currency])
138    }
139
140    fn text(&mut self, text: &Expression) {
141        match text {
142            Expression::Interpolated(parts) => parts.iter().for_each(|part| self.text(part)),
143            Expression::Call(call) => self.call(call),
144            _ => {}
145        }
146    }
147
148    fn call(&mut self, call: &Call) {
149        let position = call.position();
150        let (key, kind) = match call {
151            Call::Translate { key, arguments, .. } => {
152                let mut names: Vec<String> = arguments
153                    .iter()
154                    .map(|argument| argument.name.clone())
155                    .collect();
156                names.sort_unstable();
157                for argument in arguments {
158                    self.text(&argument.value);
159                }
160                (key, KeyKind::Message { arguments: names })
161            }
162            Call::Label { key, .. } => (key, KeyKind::Label),
163            Call::Format { .. } => return,
164        };
165        self.uses.push(KeyUse {
166            key: key.clone(),
167            kind,
168            line: position.line,
169            column: position.column,
170        });
171    }
172
173    fn enumeration(&mut self, name: &str, position: Position) {
174        self.uses.push(KeyUse {
175            key: name.to_owned(),
176            kind: KeyKind::Enum,
177            line: position.line,
178            column: position.column,
179        });
180    }
181}
182
183fn refuse_calls<const N: usize>(expressions: [Option<&Expression>; N]) -> Result<(), LocatedError> {
184    expressions.into_iter().flatten().try_for_each(refuse_call)
185}
186
187fn refuse_call(expression: &Expression) -> Result<(), LocatedError> {
188    match expression {
189        Expression::Call(call) => Err(LocatedError::new(
190            call.position(),
191            "`t(…)`, formats and labels write text; they cannot be a value, a URL or an id",
192        )),
193        Expression::Interpolated(parts) => parts.iter().try_for_each(refuse_call),
194        _ => Ok(()),
195    }
196}