1use std::collections::BTreeMap;
4use std::path::Path;
5use std::sync::Arc;
6
7use mos_core::{
8 AttrMap, AttrValue, Diagnostic, Document, NodeId, NodeKind, NodeSpec, SourceSpan, codes,
9};
10use mos_parse::{SetArg, SetValue};
11
12use crate::{image, insert_label_attributes, set::coerce_positive_length};
13
14pub(super) fn lower_image_directive(
19 document: &mut Document,
20 root: NodeId,
21 args: &[SetArg],
22 span: &SourceSpan,
23 source_file: &Path,
24 em_pt: f64,
25 diagnostics: &mut Vec<Diagnostic>,
26) {
27 let Some((attributes, _label)) =
28 build_image_attributes(args, span, source_file, em_pt, diagnostics)
29 else {
30 return;
31 };
32 document.alloc_child(
33 root,
34 NodeSpec::new(NodeKind::Image, span.clone()).with_attributes(attributes),
35 );
36}
37
38pub(super) fn lower_figure_directive(
43 document: &mut Document,
44 root: NodeId,
45 args: &[SetArg],
46 span: &SourceSpan,
47 source_file: &Path,
48 em_pt: f64,
49 diagnostics: &mut Vec<Diagnostic>,
50) {
51 let mut image_args: Vec<SetArg> = Vec::new();
58 let mut caption: Option<(String, SourceSpan)> = None;
59 let mut figure_label: Option<(String, SourceSpan)> = None;
60 let mut numbered: Option<bool> = None;
64 let mut supplement: Option<String> = None;
65 for arg in args {
66 match arg {
67 SetArg::Positional { .. } => image_args.push(arg.clone()),
72 SetArg::Named {
73 key,
74 value,
75 key_span,
76 value_span,
77 } => match key.as_str() {
78 "image" => {
79 image_args.push(SetArg::Positional {
82 value: value.clone(),
83 value_span: value_span.clone(),
84 });
85 }
86 "width" | "height" | "alt" => {
87 image_args.push(arg.clone());
88 }
89 "caption" => match value {
90 SetValue::Str(s) => {
91 caption = Some((s.clone(), value_span.clone()));
92 }
93 _ => diagnostics.push(
94 Diagnostic::simple(&codes::MOS0020, None,
95 "`#figure(caption: ...)` expects a string",
96 )
97 .with_span(value_span.clone()),
98 ),
99 },
100 "label" => match value {
101 SetValue::Str(s) => {
102 figure_label = Some((s.clone(), string_content_span(value_span)));
103 }
104 _ => diagnostics.push(
105 Diagnostic::simple(&codes::MOS0020, None,
106 "`#figure(label: ...)` expects a string",
107 )
108 .with_span(value_span.clone()),
109 ),
110 },
111 "numbered" => match value {
115 SetValue::Ident(word) if word == "true" => numbered = Some(true),
116 SetValue::Ident(word) if word == "false" => numbered = Some(false),
117 _ => diagnostics.push(
118 Diagnostic::simple(&codes::MOS0020, None,
119 "`#figure(numbered: ...)` expects `true` or `false`",
120 )
121 .with_span(value_span.clone()),
122 ),
123 },
124 "supplement" => match value {
129 SetValue::Str(s) => supplement = Some(s.clone()),
130 SetValue::Ident(word) if word == "none" => supplement = Some(String::new()),
131 _ => diagnostics.push(
132 Diagnostic::simple(&codes::MOS0020, None,
133 "`#figure(supplement: ...)` expects a string or `none`",
134 )
135 .with_span(value_span.clone()),
136 ),
137 },
138 _ => diagnostics.push(
139 Diagnostic::simple(&codes::MOS0015, None,
140 format!(
141 "unknown argument `{key}` for `#figure` (valid: image, caption, alt, width, height, label, numbered, supplement)"
142 ),
143 )
144 .with_span(key_span.clone()),
145 ),
146 },
147 }
148 }
149
150 let Some((image_attrs, _label)) =
153 build_image_attributes(&image_args, span, source_file, em_pt, diagnostics)
154 else {
155 return;
156 };
157
158 let mut figure_attrs: AttrMap = BTreeMap::new();
159 if let Some((label, label_span)) = figure_label {
160 insert_label_attributes(&mut figure_attrs, &label, Some(&label_span));
161 }
162 if numbered == Some(false) {
164 figure_attrs.insert("numbered".to_owned(), AttrValue::Bool(false));
165 }
166 if let Some(supp) = supplement {
167 figure_attrs.insert("supplement".to_owned(), AttrValue::Str(supp));
168 }
169 let figure_id = document.alloc_child(
170 root,
171 NodeSpec::new(NodeKind::Figure, span.clone()).with_attributes(figure_attrs),
172 );
173 document.alloc_child(
174 figure_id,
175 NodeSpec::new(NodeKind::Image, span.clone()).with_attributes(image_attrs),
176 );
177 if let Some((text, caption_span)) = caption {
178 let caption_id = document.alloc_child(
179 figure_id,
180 NodeSpec::new(NodeKind::Paragraph, caption_span.clone()).with_attributes({
181 let mut a = AttrMap::new();
182 a.insert("role".to_owned(), AttrValue::Str("caption".to_owned()));
186 a
187 }),
188 );
189 let mut child_attrs = AttrMap::new();
190 child_attrs.insert("text".to_owned(), AttrValue::Str(text));
191 document.alloc_child(
192 caption_id,
193 NodeSpec::new(NodeKind::Text, caption_span).with_attributes(child_attrs),
194 );
195 }
196}
197
198fn build_image_attributes(
204 args: &[SetArg],
205 span: &SourceSpan,
206 source_file: &Path,
207 em_pt: f64,
208 diagnostics: &mut Vec<Diagnostic>,
209) -> Option<(AttrMap, Option<String>)> {
210 let mut src_path: Option<(String, SourceSpan)> = None;
211 let mut alt: Option<String> = None;
212 let mut declared_width: Option<f64> = None;
213 let mut declared_height: Option<f64> = None;
214 let mut label: Option<(String, SourceSpan)> = None;
215 for arg in args {
216 match arg {
217 SetArg::Positional { value, value_span } => match value {
219 SetValue::Str(s) => src_path = Some((s.clone(), value_span.clone())),
220 _ => diagnostics.push(
221 Diagnostic::simple(&codes::MOS0020, None,
222 "`#image(...)` expects a string path",
223 )
224 .with_span(value_span.clone()),
225 ),
226 },
227 SetArg::Named {
228 key,
229 value,
230 key_span,
231 value_span,
232 } => match key.as_str() {
233 "src" | "path" => match value {
234 SetValue::Str(s) => src_path = Some((s.clone(), value_span.clone())),
235 _ => diagnostics.push(
236 Diagnostic::simple(&codes::MOS0020, None,
237 "`#image(...)` expects a string path",
238 )
239 .with_span(value_span.clone()),
240 ),
241 },
242 "alt" => match value {
243 SetValue::Str(s) => alt = Some(s.clone()),
244 _ => diagnostics.push(
245 Diagnostic::simple(&codes::MOS0020, None,
246 "`#image(alt: ...)` expects a string",
247 )
248 .with_span(value_span.clone()),
249 ),
250 },
251 "width" => {
252 if let Some(v) =
253 coerce_positive_length(value, em_pt, "width", value_span, diagnostics)
254 {
255 declared_width = Some(v);
256 }
257 }
258 "height" => {
259 if let Some(v) =
260 coerce_positive_length(value, em_pt, "height", value_span, diagnostics)
261 {
262 declared_height = Some(v);
263 }
264 }
265 "label" => match value {
266 SetValue::Str(s) => label = Some((s.clone(), string_content_span(value_span))),
267 _ => diagnostics.push(
268 Diagnostic::simple(&codes::MOS0020, None,
269 "`#image(label: ...)` expects a string",
270 )
271 .with_span(value_span.clone()),
272 ),
273 },
274 _ => diagnostics.push(
275 Diagnostic::simple(&codes::MOS0015, None,
276 format!(
277 "unknown argument `{key}` for `#image` (valid: src/path, alt, width, height, label)"
278 ),
279 )
280 .with_span(key_span.clone()),
281 ),
282 },
283 }
284 }
285 let Some((path, _path_span)) = src_path else {
286 diagnostics.push(
287 Diagnostic::simple(
288 &codes::MOS0037,
289 None,
290 "`#image(...)` requires a path (e.g. `#image(\"scan.png\")`)",
291 )
292 .with_span(span.clone()),
293 );
294 return None;
295 };
296 if path.trim().is_empty() {
300 diagnostics.push(
301 Diagnostic::simple(
302 &codes::MOS0037,
303 None,
304 "`#image(...)` requires a non-empty path (e.g. `#image(\"scan.png\")`)",
305 )
306 .with_span(span.clone()),
307 );
308 return None;
309 }
310 let (resolved, decoded) = match image::load(&path, source_file, span) {
311 Ok(v) => v,
312 Err(diag) => {
313 diagnostics.push(*diag);
314 return None;
315 }
316 };
317
318 let mut attrs: AttrMap = BTreeMap::new();
319 attrs.insert("src".to_owned(), AttrValue::Str(path));
320 attrs.insert(
321 "resolved_path".to_owned(),
322 AttrValue::Str(resolved.to_string_lossy().into_owned()),
323 );
324 if let Some(a) = alt {
325 attrs.insert("alt".to_owned(), AttrValue::Str(a));
326 }
327 if let Some(w) = declared_width {
328 attrs.insert("width".to_owned(), AttrValue::Length(w));
329 }
330 if let Some(h) = declared_height {
331 attrs.insert("height".to_owned(), AttrValue::Length(h));
332 }
333 if let Some((label_text, label_span)) = &label {
334 insert_label_attributes(&mut attrs, label_text, Some(label_span));
335 }
336 attrs.insert(
337 "pixel_width".to_owned(),
338 AttrValue::Int(i64::from(decoded.width)),
339 );
340 attrs.insert(
341 "pixel_height".to_owned(),
342 AttrValue::Int(i64::from(decoded.height)),
343 );
344 attrs.insert(
345 "color_space".to_owned(),
346 AttrValue::Str("DeviceRGB".to_owned()),
347 );
348 attrs.insert("bits_per_component".to_owned(), AttrValue::Int(8));
349 attrs.insert(
350 "pixels".to_owned(),
351 AttrValue::Bytes(Arc::from(decoded.rgb8)),
352 );
353 Some((attrs, label.map(|(text, _)| text)))
354}
355
356fn string_content_span(value_span: &SourceSpan) -> SourceSpan {
357 if value_span.end() > value_span.start().saturating_add(1) {
358 SourceSpan::new(
359 value_span.file.clone(),
360 value_span.start() + 1,
361 value_span.end() - 1,
362 )
363 } else {
364 value_span.clone()
365 }
366}