1use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use mos_core::{
7 AttrMap, AttrValue, Diagnostic, Document, NodeId, NodeKind, NodeSpec, SourceSpan, codes,
8};
9use mos_parse::{SetArg, SetValue};
10
11use crate::dependency::ExternalInputs;
12use crate::{
13 image, insert_label_attributes, set::coerce_positive_length, string_content_span, suggest,
14};
15
16pub(crate) const PIXELS_ATTR: &str = "pixels";
18pub(crate) const PIXEL_WIDTH_ATTR: &str = "pixel_width";
19pub(crate) const PIXEL_HEIGHT_ATTR: &str = "pixel_height";
20pub(crate) const COLOR_SPACE_ATTR: &str = "color_space";
21pub(crate) const BITS_PER_COMPONENT_ATTR: &str = "bits_per_component";
22
23const FIGURE_KEYS: &[&str] = &[
26 "image",
27 "caption",
28 "alt",
29 "width",
30 "height",
31 "label",
32 "numbered",
33 "supplement",
34];
35
36const IMAGE_KEYS: &[&str] = &["src", "path", "alt", "width", "height", "label"];
39
40pub(crate) fn lower_image_directive(
45 document: &mut Document,
46 root: NodeId,
47 args: &[SetArg],
48 span: &SourceSpan,
49 inputs: &mut ExternalInputs<'_>,
50 em_pt: f64,
51 diagnostics: &mut Vec<Diagnostic>,
52) {
53 let Some((attributes, _label)) = build_image_attributes(args, span, inputs, em_pt, diagnostics)
54 else {
55 return;
56 };
57 document.alloc_child(
58 root,
59 NodeSpec::new(NodeKind::Image, span.clone()).with_attributes(attributes),
60 );
61}
62
63pub(crate) fn lower_figure_directive(
67 document: &mut Document,
68 root: NodeId,
69 args: &[SetArg],
70 span: &SourceSpan,
71 inputs: &mut ExternalInputs<'_>,
72 em_pt: f64,
73 diagnostics: &mut Vec<Diagnostic>,
74) {
75 let figure_args = collect_figure_args(args, diagnostics);
76
77 let Some((image_attrs, _label)) =
80 build_image_attributes(&figure_args.image_args, span, inputs, em_pt, diagnostics)
81 else {
82 return;
83 };
84
85 let mut figure_attrs: AttrMap = BTreeMap::new();
86 if let Some((label, label_span)) = figure_args.label {
87 insert_label_attributes(&mut figure_attrs, &label, Some(&label_span));
88 }
89 if figure_args.numbered == Some(false) {
91 figure_attrs.insert("numbered".to_owned(), AttrValue::Bool(false));
92 }
93 if let Some(supp) = figure_args.supplement {
94 figure_attrs.insert("supplement".to_owned(), AttrValue::Str(supp));
95 }
96 let figure_id = document.alloc_child(
97 root,
98 NodeSpec::new(NodeKind::Figure, span.clone()).with_attributes(figure_attrs),
99 );
100 document.alloc_child(
101 figure_id,
102 NodeSpec::new(NodeKind::Image, span.clone()).with_attributes(image_attrs),
103 );
104 if let Some(caption) = figure_args.caption {
105 append_caption(document, figure_id, caption);
106 }
107}
108
109struct FigureDirectiveArgs {
110 image_args: Vec<SetArg>,
111 caption: Option<(String, SourceSpan)>,
112 label: Option<(String, SourceSpan)>,
113 numbered: Option<bool>,
114 supplement: Option<String>,
115}
116
117fn collect_figure_args(args: &[SetArg], diagnostics: &mut Vec<Diagnostic>) -> FigureDirectiveArgs {
118 let mut collected = FigureDirectiveArgs {
119 image_args: Vec::new(),
120 caption: None,
121 label: None,
122 numbered: None,
123 supplement: None,
124 };
125 for arg in args {
126 collect_one_figure_arg(arg, &mut collected, diagnostics);
127 }
128 collected
129}
130
131fn collect_one_figure_arg(
132 arg: &SetArg,
133 collected: &mut FigureDirectiveArgs,
134 diagnostics: &mut Vec<Diagnostic>,
135) {
136 match arg {
137 SetArg::Positional { .. } => collected.image_args.push(arg.clone()),
138 SetArg::Named {
139 key,
140 value,
141 key_span,
142 value_span,
143 } => match key.as_str() {
144 "image" => collected.image_args.push(SetArg::Positional {
145 value: value.clone(),
146 value_span: value_span.clone(),
147 }),
148 "width" | "height" | "alt" => collected.image_args.push(arg.clone()),
149 "caption" => collect_string_arg(
150 value,
151 value_span,
152 "`#figure(caption: ...)` expects a string",
153 &mut collected.caption,
154 diagnostics,
155 ),
156 "label" => match value {
157 SetValue::Str(s) => {
158 collected.label = Some((s.clone(), string_content_span(value_span)));
159 }
160 _ => type_error(
161 value_span,
162 "`#figure(label: ...)` expects a string",
163 diagnostics,
164 ),
165 },
166 "numbered" => collect_numbered(value, value_span, collected, diagnostics),
167 "supplement" => collect_supplement(value, value_span, collected, diagnostics),
168 _ => diagnostics.push(suggest::unknown_key_diagnostic(
169 format!(
170 "unknown argument `{key}` for `#figure` (valid: {})",
171 FIGURE_KEYS.join(", ")
172 ),
173 key,
174 key_span,
175 FIGURE_KEYS,
176 )),
177 },
178 }
179}
180
181fn collect_string_arg(
182 value: &SetValue,
183 value_span: &SourceSpan,
184 message: &'static str,
185 target: &mut Option<(String, SourceSpan)>,
186 diagnostics: &mut Vec<Diagnostic>,
187) {
188 match value {
189 SetValue::Str(s) => *target = Some((s.clone(), value_span.clone())),
190 _ => type_error(value_span, message, diagnostics),
191 }
192}
193
194fn collect_numbered(
195 value: &SetValue,
196 value_span: &SourceSpan,
197 collected: &mut FigureDirectiveArgs,
198 diagnostics: &mut Vec<Diagnostic>,
199) {
200 match value {
201 SetValue::Ident(word) if word == "true" => collected.numbered = Some(true),
202 SetValue::Ident(word) if word == "false" => collected.numbered = Some(false),
203 _ => type_error(
204 value_span,
205 "`#figure(numbered: ...)` expects `true` or `false`",
206 diagnostics,
207 ),
208 }
209}
210
211fn collect_supplement(
212 value: &SetValue,
213 value_span: &SourceSpan,
214 collected: &mut FigureDirectiveArgs,
215 diagnostics: &mut Vec<Diagnostic>,
216) {
217 match value {
218 SetValue::Str(s) => collected.supplement = Some(s.clone()),
219 SetValue::Ident(word) if word == "none" => collected.supplement = Some(String::new()),
220 _ => type_error(
221 value_span,
222 "`#figure(supplement: ...)` expects a string or `none`",
223 diagnostics,
224 ),
225 }
226}
227
228fn append_caption(document: &mut Document, figure_id: NodeId, caption: (String, SourceSpan)) {
229 let (text, caption_span) = caption;
230 let caption_id = document.alloc_child(
231 figure_id,
232 NodeSpec::new(NodeKind::Paragraph, caption_span.clone()).with_attributes({
233 let mut attrs = AttrMap::new();
234 attrs.insert("role".to_owned(), AttrValue::Str("caption".to_owned()));
235 attrs
236 }),
237 );
238 let mut child_attrs = AttrMap::new();
239 child_attrs.insert("text".to_owned(), AttrValue::Str(text));
240 document.alloc_child(
241 caption_id,
242 NodeSpec::new(NodeKind::Text, caption_span).with_attributes(child_attrs),
243 );
244}
245
246fn build_image_attributes(
252 args: &[SetArg],
253 span: &SourceSpan,
254 inputs: &mut ExternalInputs<'_>,
255 em_pt: f64,
256 diagnostics: &mut Vec<Diagnostic>,
257) -> Option<(AttrMap, Option<String>)> {
258 let image_args = collect_image_args(args, em_pt, diagnostics);
259 let Some((path, path_span)) = image_args.src_path else {
260 diagnostics.push(
261 Diagnostic::simple(
262 &codes::MOS0037,
263 None,
264 "`#image(...)` requires a path (e.g. `#image(\"scan.png\")`)",
265 )
266 .with_span(span.clone()),
267 );
268 return None;
269 };
270 if path.trim().is_empty() {
274 diagnostics.push(
275 Diagnostic::simple(
276 &codes::MOS0037,
277 None,
278 "`#image(...)` requires a non-empty path (e.g. `#image(\"scan.png\")`)",
279 )
280 .with_span(span.clone()),
281 );
282 return None;
283 }
284 let (resolved, decoded) = match image::load(&path, inputs, span, &path_span) {
285 Ok(v) => v,
286 Err(diag) => {
287 diagnostics.push(*diag);
288 return None;
289 }
290 };
291
292 let mut attrs: AttrMap = BTreeMap::new();
293 attrs.insert("src".to_owned(), AttrValue::Str(path));
294 attrs.insert(
295 "resolved_path".to_owned(),
296 AttrValue::Str(resolved.to_string_lossy().into_owned()),
297 );
298 if let Some(a) = image_args.alt {
299 attrs.insert("alt".to_owned(), AttrValue::Str(a));
300 }
301 if let Some(w) = image_args.declared_width {
302 attrs.insert("width".to_owned(), AttrValue::Length(w));
303 }
304 if let Some(h) = image_args.declared_height {
305 attrs.insert("height".to_owned(), AttrValue::Length(h));
306 }
307 if let Some((label_text, label_span)) = &image_args.label {
308 insert_label_attributes(&mut attrs, label_text, Some(label_span));
309 }
310 attrs.insert(
311 PIXEL_WIDTH_ATTR.to_owned(),
312 AttrValue::Int(i64::from(decoded.width)),
313 );
314 attrs.insert(
315 PIXEL_HEIGHT_ATTR.to_owned(),
316 AttrValue::Int(i64::from(decoded.height)),
317 );
318 attrs.insert(
319 COLOR_SPACE_ATTR.to_owned(),
320 AttrValue::Str("DeviceRGB".to_owned()),
321 );
322 attrs.insert(BITS_PER_COMPONENT_ATTR.to_owned(), AttrValue::Int(8));
323 attrs.insert(
324 PIXELS_ATTR.to_owned(),
325 AttrValue::Bytes(Arc::from(decoded.rgb8)),
326 );
327 Some((attrs, image_args.label.map(|(text, _)| text)))
328}
329
330struct ImageDirectiveArgs {
331 src_path: Option<(String, SourceSpan)>,
332 alt: Option<String>,
333 declared_width: Option<f64>,
334 declared_height: Option<f64>,
335 label: Option<(String, SourceSpan)>,
336}
337
338fn collect_image_args(
339 args: &[SetArg],
340 em_pt: f64,
341 diagnostics: &mut Vec<Diagnostic>,
342) -> ImageDirectiveArgs {
343 let mut collected = ImageDirectiveArgs {
344 src_path: None,
345 alt: None,
346 declared_width: None,
347 declared_height: None,
348 label: None,
349 };
350 for arg in args {
351 collect_one_image_arg(arg, em_pt, &mut collected, diagnostics);
352 }
353 collected
354}
355
356fn collect_one_image_arg(
357 arg: &SetArg,
358 em_pt: f64,
359 collected: &mut ImageDirectiveArgs,
360 diagnostics: &mut Vec<Diagnostic>,
361) {
362 match arg {
363 SetArg::Positional { value, value_span } => collect_image_path(
364 value,
365 value_span,
366 &mut collected.src_path,
367 diagnostics,
368 ),
369 SetArg::Named {
370 key,
371 value,
372 key_span,
373 value_span,
374 } => match key.as_str() {
375 "src" | "path" => collect_image_path(
376 value,
377 value_span,
378 &mut collected.src_path,
379 diagnostics,
380 ),
381 "alt" => match value {
382 SetValue::Str(s) => collected.alt = Some(s.clone()),
383 _ => type_error(value_span, "`#image(alt: ...)` expects a string", diagnostics),
384 },
385 "width" => {
386 if let Some(width) = coerce_positive_length(
387 value,
388 em_pt,
389 "width",
390 value_span,
391 diagnostics,
392 ) {
393 collected.declared_width = Some(width);
394 }
395 }
396 "height" => {
397 if let Some(height) = coerce_positive_length(
398 value,
399 em_pt,
400 "height",
401 value_span,
402 diagnostics,
403 ) {
404 collected.declared_height = Some(height);
405 }
406 }
407 "label" => match value {
408 SetValue::Str(s) => {
409 collected.label = Some((s.clone(), string_content_span(value_span)));
410 }
411 _ => type_error(value_span, "`#image(label: ...)` expects a string", diagnostics),
412 },
413 _ => diagnostics.push(suggest::unknown_key_diagnostic(
417 format!(
418 "unknown argument `{key}` for `#image` (valid: src/path, alt, width, height, label)"
419 ),
420 key,
421 key_span,
422 IMAGE_KEYS,
423 )),
424 },
425 }
426}
427
428fn collect_image_path(
429 value: &SetValue,
430 value_span: &SourceSpan,
431 target: &mut Option<(String, SourceSpan)>,
432 diagnostics: &mut Vec<Diagnostic>,
433) {
434 match value {
435 SetValue::Str(s) => *target = Some((s.clone(), value_span.clone())),
436 _ => type_error(
437 value_span,
438 "`#image(...)` expects a string path",
439 diagnostics,
440 ),
441 }
442}
443
444fn type_error(value_span: &SourceSpan, message: &'static str, diagnostics: &mut Vec<Diagnostic>) {
445 diagnostics
446 .push(Diagnostic::simple(&codes::MOS0020, None, message).with_span(value_span.clone()));
447}