1use serde::{Deserialize, Serialize};
25
26use super::inline::WireInline;
27use super::range::Range;
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34#[serde(tag = "kind", rename_all = "snake_case")]
35#[non_exhaustive]
36pub enum WireNode {
37 Document {
38 range: Range,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 origin: Option<String>,
41 children: Vec<WireNode>,
42 },
43 Session {
44 range: Range,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 origin: Option<String>,
47 title: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 marker: Option<String>,
50 children: Vec<WireNode>,
51 },
52 Definition {
53 range: Range,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 origin: Option<String>,
56 subject: String,
57 children: Vec<WireNode>,
58 },
59 Paragraph {
60 range: Range,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 origin: Option<String>,
63 inlines: Vec<WireInline>,
64 },
65 List {
66 range: Range,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 origin: Option<String>,
69 marker_style: String,
70 items: Vec<WireListItem>,
71 },
72 Verbatim {
73 range: Range,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 origin: Option<String>,
76 label: String,
77 params: serde_json::Value,
78 body_text: String,
79 #[serde(default, skip_serializing_if = "String::is_empty")]
84 subject: String,
85 #[serde(default = "default_verbatim_mode")]
91 mode: String,
92 },
93 Table {
94 range: Range,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 origin: Option<String>,
97 caption: String,
98 header_rows: u32,
99 column_aligns: Vec<String>,
110 rows: Vec<WireRow>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 footnotes: Vec<WireFootnote>,
113 },
114 Image {
119 range: Range,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 origin: Option<String>,
122 src: String,
123 #[serde(default, skip_serializing_if = "String::is_empty")]
124 alt: String,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 title: Option<String>,
127 },
128 Video {
130 range: Range,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 origin: Option<String>,
133 src: String,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 title: Option<String>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 poster: Option<String>,
138 },
139 Audio {
141 range: Range,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 origin: Option<String>,
144 src: String,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 title: Option<String>,
147 },
148 Annotation {
149 range: Range,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 origin: Option<String>,
152 label: String,
153 params: serde_json::Value,
154 body: serde_json::Value,
159 },
160 Blank {
161 range: Range,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 origin: Option<String>,
164 },
165}
166
167impl WireNode {
168 pub fn range(&self) -> Range {
173 match self {
174 Self::Document { range, .. }
175 | Self::Session { range, .. }
176 | Self::Definition { range, .. }
177 | Self::Paragraph { range, .. }
178 | Self::List { range, .. }
179 | Self::Verbatim { range, .. }
180 | Self::Table { range, .. }
181 | Self::Image { range, .. }
182 | Self::Video { range, .. }
183 | Self::Audio { range, .. }
184 | Self::Annotation { range, .. }
185 | Self::Blank { range, .. } => *range,
186 }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct WireListItem {
193 pub range: Range,
194 pub inlines: Vec<WireInline>,
195 #[serde(default, skip_serializing_if = "Vec::is_empty")]
196 pub children: Vec<WireNode>,
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub struct WireRow {
202 pub cells: Vec<WireTableCell>,
203}
204
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209pub struct WireTableCell {
210 pub inlines: Vec<WireInline>,
211 #[serde(default = "one")]
212 pub colspan: u32,
213 #[serde(default = "one")]
214 pub rowspan: u32,
215}
216
217fn one() -> u32 {
218 1
219}
220
221fn default_verbatim_mode() -> String {
222 "inflow".to_string()
223}
224
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
227pub struct WireFootnote {
228 pub marker: String,
229 pub inlines: Vec<WireInline>,
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::wire::range::Position;
236
237 fn r(s_l: u32, s_c: u32, e_l: u32, e_c: u32) -> Range {
238 Range::new(Position::new(s_l, s_c), Position::new(e_l, e_c))
239 }
240
241 #[test]
242 fn paragraph_round_trips() {
243 let p = WireNode::Paragraph {
244 range: r(0, 0, 0, 5),
245 origin: None,
246 inlines: vec![WireInline::Text {
247 text: "hello".into(),
248 }],
249 };
250 let s = serde_json::to_string(&p).unwrap();
251 let back: WireNode = serde_json::from_str(&s).unwrap();
252 assert_eq!(back, p);
253 }
254
255 #[test]
256 fn paragraph_kind_in_serialized_form() {
257 let p = WireNode::Paragraph {
258 range: r(0, 0, 0, 5),
259 origin: None,
260 inlines: vec![],
261 };
262 let s = serde_json::to_string(&p).unwrap();
263 assert!(s.contains(r#""kind":"paragraph""#));
264 }
265
266 #[test]
267 fn document_with_children() {
268 let d = WireNode::Document {
269 range: r(0, 0, 10, 0),
270 origin: Some("doc.lex".into()),
271 children: vec![WireNode::Paragraph {
272 range: r(0, 0, 0, 3),
273 origin: None,
274 inlines: vec![WireInline::Text { text: "x".into() }],
275 }],
276 };
277 let s = serde_json::to_string(&d).unwrap();
278 assert!(s.contains(r#""origin":"doc.lex""#));
279 let back: WireNode = serde_json::from_str(&s).unwrap();
280 assert_eq!(back, d);
281 }
282
283 #[test]
284 fn annotation_with_lex_body() {
285 let a = WireNode::Annotation {
286 range: r(3, 0, 6, 0),
287 origin: None,
288 label: "acme.commenting".into(),
289 params: serde_json::json!({"role": "editor"}),
290 body: serde_json::json!({
291 "kind": "block",
292 "children": []
293 }),
294 };
295 let s = serde_json::to_string(&a).unwrap();
296 let back: WireNode = serde_json::from_str(&s).unwrap();
297 assert_eq!(back, a);
298 }
299
300 #[test]
301 fn verbatim_carries_label_and_body_text() {
302 let v = WireNode::Verbatim {
303 range: r(0, 0, 4, 0),
304 origin: None,
305 label: "rust".into(),
306 params: serde_json::json!({}),
307 body_text: "fn main() {}".into(),
308 subject: "Code:".into(),
309 mode: "inflow".into(),
310 };
311 let s = serde_json::to_string(&v).unwrap();
312 let back: WireNode = serde_json::from_str(&s).unwrap();
313 assert_eq!(back, v);
314 }
315
316 #[test]
317 fn verbatim_mode_field_defaults_to_inflow_on_deserialise() {
318 let payload = r#"{
322 "kind":"verbatim",
323 "range":{"start":[0,0],"end":[4,0]},
324 "label":"rust",
325 "params":{},
326 "body_text":"x"
327 }"#;
328 let v: WireNode = serde_json::from_str(payload).unwrap();
329 match v {
330 WireNode::Verbatim {
331 ref mode,
332 ref subject,
333 ..
334 } => {
335 assert_eq!(mode, "inflow");
336 assert_eq!(subject, "");
337 }
338 _ => panic!("expected Verbatim"),
339 }
340 }
341
342 #[test]
343 fn table_round_trips_with_per_column_aligns() {
344 let t = WireNode::Table {
350 range: r(0, 0, 3, 0),
351 origin: None,
352 caption: "Demo".into(),
353 header_rows: 1,
354 column_aligns: vec!["left".into(), "center".into(), "right".into()],
355 rows: vec![
356 WireRow {
357 cells: vec![
358 WireTableCell {
359 inlines: vec![WireInline::Text { text: "h1".into() }],
360 colspan: 1,
361 rowspan: 1,
362 },
363 WireTableCell {
364 inlines: vec![WireInline::Text { text: "h2".into() }],
365 colspan: 1,
366 rowspan: 1,
367 },
368 WireTableCell {
369 inlines: vec![WireInline::Text { text: "h3".into() }],
370 colspan: 1,
371 rowspan: 1,
372 },
373 ],
374 },
375 WireRow {
376 cells: vec![
377 WireTableCell {
378 inlines: vec![WireInline::Text { text: "c1".into() }],
379 colspan: 1,
380 rowspan: 1,
381 },
382 WireTableCell {
383 inlines: vec![WireInline::Text { text: "c2".into() }],
384 colspan: 1,
385 rowspan: 1,
386 },
387 WireTableCell {
388 inlines: vec![WireInline::Text { text: "c3".into() }],
389 colspan: 1,
390 rowspan: 1,
391 },
392 ],
393 },
394 ],
395 footnotes: vec![],
396 };
397 let s = serde_json::to_string(&t).unwrap();
398 assert!(
399 s.contains(r#""column_aligns":["left","center","right"]"#),
400 "column_aligns must serialize as an array of per-column strings, got: {s}"
401 );
402 let back: WireNode = serde_json::from_str(&s).unwrap();
403 assert_eq!(back, t);
404 }
405
406 #[test]
407 fn image_round_trips() {
408 let i = WireNode::Image {
409 range: r(2, 0, 2, 30),
410 origin: None,
411 src: "chart.png".into(),
412 alt: "Q4 chart".into(),
413 title: Some("Quarter".into()),
414 };
415 let s = serde_json::to_string(&i).unwrap();
416 assert!(s.contains(r#""kind":"image""#));
417 assert!(s.contains(r#""src":"chart.png""#));
418 let back: WireNode = serde_json::from_str(&s).unwrap();
419 assert_eq!(back, i);
420 }
421
422 #[test]
423 fn image_marker_form_omits_empty_alt() {
424 let i = WireNode::Image {
425 range: r(0, 0, 0, 0),
426 origin: None,
427 src: "x.png".into(),
428 alt: String::new(),
429 title: None,
430 };
431 let s = serde_json::to_string(&i).unwrap();
432 assert!(!s.contains("alt"), "empty alt must be omitted: {s}");
433 assert!(!s.contains("title"), "None title must be omitted: {s}");
434 }
435
436 #[test]
437 fn video_round_trips_with_poster() {
438 let v = WireNode::Video {
439 range: r(0, 0, 0, 0),
440 origin: None,
441 src: "demo.mp4".into(),
442 title: Some("Demo".into()),
443 poster: Some("frame.png".into()),
444 };
445 let s = serde_json::to_string(&v).unwrap();
446 assert!(s.contains(r#""kind":"video""#));
447 assert!(s.contains(r#""poster":"frame.png""#));
448 let back: WireNode = serde_json::from_str(&s).unwrap();
449 assert_eq!(back, v);
450 }
451
452 #[test]
453 fn audio_round_trips() {
454 let a = WireNode::Audio {
455 range: r(0, 0, 0, 0),
456 origin: None,
457 src: "track.mp3".into(),
458 title: None,
459 };
460 let s = serde_json::to_string(&a).unwrap();
461 assert!(s.contains(r#""kind":"audio""#));
462 let back: WireNode = serde_json::from_str(&s).unwrap();
463 assert_eq!(back, a);
464 }
465
466 #[test]
467 fn list_with_items() {
468 let l = WireNode::List {
469 range: r(0, 0, 2, 0),
470 origin: None,
471 marker_style: "dash".into(),
472 items: vec![
473 WireListItem {
474 range: r(0, 0, 0, 5),
475 inlines: vec![WireInline::Text { text: "a".into() }],
476 children: vec![],
477 },
478 WireListItem {
479 range: r(1, 0, 1, 5),
480 inlines: vec![WireInline::Text { text: "b".into() }],
481 children: vec![],
482 },
483 ],
484 };
485 let s = serde_json::to_string(&l).unwrap();
486 let back: WireNode = serde_json::from_str(&s).unwrap();
487 assert_eq!(back, l);
488 }
489}