sofie_engine/translations/
uses.rs1use 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
24pub(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(§ion.title);
66 self.ops(§ion.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}