1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct AdfDocument {
11 pub version: u32,
13
14 #[serde(rename = "type")]
16 pub doc_type: String,
17
18 pub content: Vec<AdfNode>,
20}
21
22impl AdfDocument {
23 #[must_use]
25 pub fn new() -> Self {
26 Self {
27 version: 1,
28 doc_type: "doc".to_string(),
29 content: Vec::new(),
30 }
31 }
32
33 pub fn from_json_str(input: &str) -> anyhow::Result<Self> {
40 use anyhow::Context;
41
42 let value: serde_json::Value =
43 serde_json::from_str(input).context("Failed to parse ADF JSON input")?;
44 if value.is_null() {
45 return Ok(Self::default());
46 }
47 serde_json::from_value(value).context("Failed to parse ADF JSON input")
48 }
49}
50
51impl Default for AdfDocument {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62pub struct AdfNode {
63 #[serde(rename = "type")]
65 pub node_type: String,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub attrs: Option<serde_json::Value>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub content: Option<Vec<Self>>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub text: Option<String>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub marks: Option<Vec<AdfMark>>,
82
83 #[serde(rename = "localId", skip_serializing_if = "Option::is_none")]
85 pub local_id: Option<String>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub parameters: Option<serde_json::Value>,
90}
91
92impl AdfNode {
93 #[must_use]
95 pub fn text(content: &str) -> Self {
96 Self {
97 node_type: "text".to_string(),
98 attrs: None,
99 content: None,
100 text: Some(content.to_string()),
101 marks: None,
102 local_id: None,
103 parameters: None,
104 }
105 }
106
107 #[must_use]
109 pub fn text_with_marks(content: &str, marks: Vec<AdfMark>) -> Self {
110 Self {
111 node_type: "text".to_string(),
112 attrs: None,
113 content: None,
114 text: Some(content.to_string()),
115 marks: if marks.is_empty() { None } else { Some(marks) },
116 local_id: None,
117 parameters: None,
118 }
119 }
120
121 #[must_use]
123 pub fn paragraph(content: Vec<Self>) -> Self {
124 Self {
125 node_type: "paragraph".to_string(),
126 attrs: None,
127 content: if content.is_empty() {
128 None
129 } else {
130 Some(content)
131 },
132 text: None,
133 marks: None,
134 local_id: None,
135 parameters: None,
136 }
137 }
138
139 #[must_use]
141 pub fn heading(level: u8, content: Vec<Self>) -> Self {
142 Self {
143 node_type: "heading".to_string(),
144 attrs: Some(serde_json::json!({"level": level})),
145 content: if content.is_empty() {
146 None
147 } else {
148 Some(content)
149 },
150 text: None,
151 marks: None,
152 local_id: None,
153 parameters: None,
154 }
155 }
156
157 #[must_use]
159 pub fn code_block(language: Option<&str>, text: &str) -> Self {
160 Self {
161 node_type: "codeBlock".to_string(),
162 attrs: language.map(|lang| serde_json::json!({"language": lang})),
163 content: Some(vec![Self::text(text)]),
164 text: None,
165 marks: None,
166 local_id: None,
167 parameters: None,
168 }
169 }
170
171 #[must_use]
173 pub fn blockquote(content: Vec<Self>) -> Self {
174 Self {
175 node_type: "blockquote".to_string(),
176 attrs: None,
177 content: Some(content),
178 text: None,
179 marks: None,
180 local_id: None,
181 parameters: None,
182 }
183 }
184
185 #[must_use]
187 pub fn rule() -> Self {
188 Self {
189 node_type: "rule".to_string(),
190 attrs: None,
191 content: None,
192 text: None,
193 marks: None,
194 local_id: None,
195 parameters: None,
196 }
197 }
198
199 #[must_use]
201 pub fn bullet_list(items: Vec<Self>) -> Self {
202 Self {
203 node_type: "bulletList".to_string(),
204 attrs: None,
205 content: Some(items),
206 text: None,
207 marks: None,
208 local_id: None,
209 parameters: None,
210 }
211 }
212
213 #[must_use]
215 pub fn ordered_list(items: Vec<Self>, start: Option<u32>) -> Self {
216 Self {
217 node_type: "orderedList".to_string(),
218 attrs: start.map(|s| serde_json::json!({"order": s})),
219 content: Some(items),
220 text: None,
221 marks: None,
222 local_id: None,
223 parameters: None,
224 }
225 }
226
227 #[must_use]
229 pub fn list_item(content: Vec<Self>) -> Self {
230 Self {
231 node_type: "listItem".to_string(),
232 attrs: None,
233 content: Some(content),
234 text: None,
235 marks: None,
236 local_id: None,
237 parameters: None,
238 }
239 }
240
241 #[must_use]
243 pub fn hard_break() -> Self {
244 Self {
245 node_type: "hardBreak".to_string(),
246 attrs: None,
247 content: None,
248 text: None,
249 marks: None,
250 local_id: None,
251 parameters: None,
252 }
253 }
254
255 #[must_use]
257 pub fn table(rows: Vec<Self>) -> Self {
258 Self {
259 node_type: "table".to_string(),
260 attrs: None,
261 content: Some(rows),
262 text: None,
263 marks: None,
264 local_id: None,
265 parameters: None,
266 }
267 }
268
269 #[must_use]
271 pub fn table_with_attrs(rows: Vec<Self>, attrs: serde_json::Value) -> Self {
272 Self {
273 node_type: "table".to_string(),
274 attrs: Some(attrs),
275 content: Some(rows),
276 text: None,
277 marks: None,
278 local_id: None,
279 parameters: None,
280 }
281 }
282
283 #[must_use]
285 pub fn table_row(cells: Vec<Self>) -> Self {
286 Self {
287 node_type: "tableRow".to_string(),
288 attrs: None,
289 content: Some(cells),
290 text: None,
291 marks: None,
292 local_id: None,
293 parameters: None,
294 }
295 }
296
297 #[must_use]
299 pub fn table_header(content: Vec<Self>) -> Self {
300 Self {
301 node_type: "tableHeader".to_string(),
302 attrs: None,
303 content: Some(content),
304 text: None,
305 marks: None,
306 local_id: None,
307 parameters: None,
308 }
309 }
310
311 #[must_use]
313 pub fn table_header_with_attrs(content: Vec<Self>, attrs: serde_json::Value) -> Self {
314 Self {
315 node_type: "tableHeader".to_string(),
316 attrs: Some(attrs),
317 content: Some(content),
318 text: None,
319 marks: None,
320 local_id: None,
321 parameters: None,
322 }
323 }
324
325 #[must_use]
327 pub fn table_cell(content: Vec<Self>) -> Self {
328 Self {
329 node_type: "tableCell".to_string(),
330 attrs: None,
331 content: Some(content),
332 text: None,
333 marks: None,
334 local_id: None,
335 parameters: None,
336 }
337 }
338
339 #[must_use]
341 pub fn table_cell_with_attrs(content: Vec<Self>, attrs: serde_json::Value) -> Self {
342 Self {
343 node_type: "tableCell".to_string(),
344 attrs: Some(attrs),
345 content: Some(content),
346 text: None,
347 marks: None,
348 local_id: None,
349 parameters: None,
350 }
351 }
352
353 #[must_use]
355 pub fn table_header_with_attrs_and_marks(
356 content: Vec<Self>,
357 attrs: Option<serde_json::Value>,
358 marks: Vec<AdfMark>,
359 ) -> Self {
360 Self {
361 node_type: "tableHeader".to_string(),
362 attrs,
363 content: Some(content),
364 text: None,
365 marks: if marks.is_empty() { None } else { Some(marks) },
366 local_id: None,
367 parameters: None,
368 }
369 }
370
371 #[must_use]
373 pub fn table_cell_with_attrs_and_marks(
374 content: Vec<Self>,
375 attrs: Option<serde_json::Value>,
376 marks: Vec<AdfMark>,
377 ) -> Self {
378 Self {
379 node_type: "tableCell".to_string(),
380 attrs,
381 content: Some(content),
382 text: None,
383 marks: if marks.is_empty() { None } else { Some(marks) },
384 local_id: None,
385 parameters: None,
386 }
387 }
388
389 #[must_use]
391 pub fn caption(content: Vec<Self>) -> Self {
392 Self {
393 node_type: "caption".to_string(),
394 attrs: None,
395 content: Some(content),
396 text: None,
397 marks: None,
398 local_id: None,
399 parameters: None,
400 }
401 }
402
403 #[must_use]
405 pub fn inline_card(url: &str) -> Self {
406 Self {
407 node_type: "inlineCard".to_string(),
408 attrs: Some(serde_json::json!({"url": url})),
409 content: None,
410 text: None,
411 marks: None,
412 local_id: None,
413 parameters: None,
414 }
415 }
416
417 #[must_use]
419 pub fn media_inline(attrs: serde_json::Value) -> Self {
420 Self {
421 node_type: "mediaInline".to_string(),
422 attrs: Some(attrs),
423 content: None,
424 text: None,
425 marks: None,
426 local_id: None,
427 parameters: None,
428 }
429 }
430
431 #[must_use]
433 pub fn media_single(url: &str, alt: Option<&str>) -> Self {
434 let mut media_attrs = serde_json::json!({
435 "type": "external",
436 "url": url,
437 });
438 if let Some(alt_text) = alt {
439 media_attrs["alt"] = serde_json::Value::String(alt_text.to_string());
440 }
441 Self {
442 node_type: "mediaSingle".to_string(),
443 attrs: Some(serde_json::json!({"layout": "center"})),
444 content: Some(vec![Self {
445 node_type: "media".to_string(),
446 attrs: Some(media_attrs),
447 content: None,
448 text: None,
449 marks: None,
450 local_id: None,
451 parameters: None,
452 }]),
453 text: None,
454 marks: None,
455 local_id: None,
456 parameters: None,
457 }
458 }
459
460 #[must_use]
464 pub fn task_list(items: Vec<Self>) -> Self {
465 Self {
466 node_type: "taskList".to_string(),
467 attrs: Some(serde_json::json!({"localId": uuid_placeholder()})),
468 content: Some(items),
469 text: None,
470 marks: None,
471 local_id: None,
472 parameters: None,
473 }
474 }
475
476 #[must_use]
478 pub fn task_item(state: &str, content: Vec<Self>) -> Self {
479 Self {
480 node_type: "taskItem".to_string(),
481 attrs: Some(serde_json::json!({
482 "localId": uuid_placeholder(),
483 "state": state,
484 })),
485 content: if content.is_empty() {
486 None
487 } else {
488 Some(content)
489 },
490 text: None,
491 marks: None,
492 local_id: None,
493 parameters: None,
494 }
495 }
496
497 #[must_use]
501 pub fn emoji(short_name: &str) -> Self {
502 Self {
503 node_type: "emoji".to_string(),
504 attrs: Some(serde_json::json!({"shortName": short_name})),
505 content: None,
506 text: None,
507 marks: None,
508 local_id: None,
509 parameters: None,
510 }
511 }
512
513 #[must_use]
515 pub fn status(text: &str, color: &str) -> Self {
516 Self {
517 node_type: "status".to_string(),
518 attrs: Some(serde_json::json!({
519 "text": text,
520 "color": color,
521 "localId": uuid_placeholder(),
522 })),
523 content: None,
524 text: None,
525 marks: None,
526 local_id: None,
527 parameters: None,
528 }
529 }
530
531 #[must_use]
533 pub fn date(timestamp: &str) -> Self {
534 Self {
535 node_type: "date".to_string(),
536 attrs: Some(serde_json::json!({"timestamp": timestamp})),
537 content: None,
538 text: None,
539 marks: None,
540 local_id: None,
541 parameters: None,
542 }
543 }
544
545 #[must_use]
547 pub fn placeholder(text: &str) -> Self {
548 Self {
549 node_type: "placeholder".to_string(),
550 attrs: Some(serde_json::json!({"text": text})),
551 content: None,
552 text: None,
553 marks: None,
554 local_id: None,
555 parameters: None,
556 }
557 }
558
559 #[must_use]
561 pub fn mention(id: &str, display_text: &str) -> Self {
562 Self {
563 node_type: "mention".to_string(),
564 attrs: Some(serde_json::json!({
565 "id": id,
566 "text": display_text,
567 })),
568 content: None,
569 text: None,
570 marks: None,
571 local_id: None,
572 parameters: None,
573 }
574 }
575
576 #[must_use]
580 pub fn block_card(url: &str) -> Self {
581 Self {
582 node_type: "blockCard".to_string(),
583 attrs: Some(serde_json::json!({"url": url})),
584 content: None,
585 text: None,
586 marks: None,
587 local_id: None,
588 parameters: None,
589 }
590 }
591
592 #[must_use]
594 pub fn embed_card(
595 url: &str,
596 layout: Option<&str>,
597 original_height: Option<f64>,
598 width: Option<f64>,
599 ) -> Self {
600 let mut attrs = serde_json::json!({"url": url});
601 if let Some(l) = layout {
602 attrs["layout"] = serde_json::Value::String(l.to_string());
603 }
604 if let Some(h) = original_height {
605 attrs["originalHeight"] = serde_json::json!(h);
606 }
607 if let Some(w) = width {
608 attrs["width"] = serde_json::json!(w);
609 }
610 Self {
611 node_type: "embedCard".to_string(),
612 attrs: Some(attrs),
613 content: None,
614 text: None,
615 marks: None,
616 local_id: None,
617 parameters: None,
618 }
619 }
620
621 #[must_use]
625 pub fn panel(panel_type: &str, content: Vec<Self>) -> Self {
626 Self {
627 node_type: "panel".to_string(),
628 attrs: Some(serde_json::json!({"panelType": panel_type})),
629 content: Some(content),
630 text: None,
631 marks: None,
632 local_id: None,
633 parameters: None,
634 }
635 }
636
637 #[must_use]
639 pub fn expand(title: Option<&str>, content: Vec<Self>) -> Self {
640 let attrs = title.map(|t| serde_json::json!({"title": t}));
641 Self {
642 node_type: "expand".to_string(),
643 attrs,
644 content: Some(content),
645 text: None,
646 marks: None,
647 local_id: None,
648 parameters: None,
649 }
650 }
651
652 #[must_use]
654 pub fn nested_expand(title: Option<&str>, content: Vec<Self>) -> Self {
655 let attrs = title.map(|t| serde_json::json!({"title": t}));
656 Self {
657 node_type: "nestedExpand".to_string(),
658 attrs,
659 content: Some(content),
660 text: None,
661 marks: None,
662 local_id: None,
663 parameters: None,
664 }
665 }
666
667 #[must_use]
671 pub fn layout_section(columns: Vec<Self>) -> Self {
672 Self {
673 node_type: "layoutSection".to_string(),
674 attrs: None,
675 content: Some(columns),
676 text: None,
677 marks: None,
678 local_id: None,
679 parameters: None,
680 }
681 }
682
683 #[must_use]
689 pub fn layout_column<V: Into<serde_json::Value>>(width: V, content: Vec<Self>) -> Self {
690 Self {
691 node_type: "layoutColumn".to_string(),
692 attrs: Some(serde_json::json!({"width": width.into()})),
693 content: Some(content),
694 text: None,
695 marks: None,
696 local_id: None,
697 parameters: None,
698 }
699 }
700
701 #[must_use]
705 pub fn decision_list(items: Vec<Self>) -> Self {
706 Self {
707 node_type: "decisionList".to_string(),
708 attrs: Some(serde_json::json!({"localId": uuid_placeholder()})),
709 content: Some(items),
710 text: None,
711 marks: None,
712 local_id: None,
713 parameters: None,
714 }
715 }
716
717 #[must_use]
719 pub fn decision_item(state: &str, content: Vec<Self>) -> Self {
720 Self {
721 node_type: "decisionItem".to_string(),
722 attrs: Some(serde_json::json!({
723 "localId": uuid_placeholder(),
724 "state": state,
725 })),
726 content: Some(content),
727 text: None,
728 marks: None,
729 local_id: None,
730 parameters: None,
731 }
732 }
733
734 #[must_use]
738 pub fn extension(
739 extension_type: &str,
740 extension_key: &str,
741 params: Option<serde_json::Value>,
742 ) -> Self {
743 let mut attrs = serde_json::json!({
744 "extensionType": extension_type,
745 "extensionKey": extension_key,
746 });
747 if let Some(p) = params {
748 attrs["parameters"] = p;
749 }
750 Self {
751 node_type: "extension".to_string(),
752 attrs: Some(attrs),
753 content: None,
754 text: None,
755 marks: None,
756 local_id: None,
757 parameters: None,
758 }
759 }
760
761 #[must_use]
763 pub fn bodied_extension(extension_type: &str, extension_key: &str, content: Vec<Self>) -> Self {
764 Self {
765 node_type: "bodiedExtension".to_string(),
766 attrs: Some(serde_json::json!({
767 "extensionType": extension_type,
768 "extensionKey": extension_key,
769 })),
770 content: Some(content),
771 text: None,
772 marks: None,
773 local_id: None,
774 parameters: None,
775 }
776 }
777
778 #[must_use]
780 pub fn inline_extension(
781 extension_type: &str,
782 extension_key: &str,
783 fallback_text: Option<&str>,
784 ) -> Self {
785 Self {
786 node_type: "inlineExtension".to_string(),
787 attrs: Some(serde_json::json!({
788 "extensionType": extension_type,
789 "extensionKey": extension_key,
790 })),
791 content: None,
792 text: fallback_text.map(String::from),
793 marks: None,
794 local_id: None,
795 parameters: None,
796 }
797 }
798}
799
800fn uuid_placeholder() -> String {
806 String::new()
807}
808
809#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
811pub struct AdfMark {
812 #[serde(rename = "type")]
814 pub mark_type: String,
815
816 #[serde(skip_serializing_if = "Option::is_none")]
818 pub attrs: Option<serde_json::Value>,
819}
820
821impl AdfMark {
822 #[must_use]
824 pub fn strong() -> Self {
825 Self {
826 mark_type: "strong".to_string(),
827 attrs: None,
828 }
829 }
830
831 #[must_use]
833 pub fn em() -> Self {
834 Self {
835 mark_type: "em".to_string(),
836 attrs: None,
837 }
838 }
839
840 #[must_use]
842 pub fn code() -> Self {
843 Self {
844 mark_type: "code".to_string(),
845 attrs: None,
846 }
847 }
848
849 #[must_use]
851 pub fn strike() -> Self {
852 Self {
853 mark_type: "strike".to_string(),
854 attrs: None,
855 }
856 }
857
858 #[must_use]
860 pub fn link(href: &str) -> Self {
861 Self {
862 mark_type: "link".to_string(),
863 attrs: Some(serde_json::json!({"href": href})),
864 }
865 }
866
867 #[must_use]
869 pub fn underline() -> Self {
870 Self {
871 mark_type: "underline".to_string(),
872 attrs: None,
873 }
874 }
875
876 #[must_use]
878 pub fn annotation(id: &str, annotation_type: &str) -> Self {
879 Self {
880 mark_type: "annotation".to_string(),
881 attrs: Some(serde_json::json!({"id": id, "annotationType": annotation_type})),
882 }
883 }
884
885 #[must_use]
887 pub fn text_color(color: &str) -> Self {
888 Self {
889 mark_type: "textColor".to_string(),
890 attrs: Some(serde_json::json!({"color": color})),
891 }
892 }
893
894 #[must_use]
896 pub fn background_color(color: &str) -> Self {
897 Self {
898 mark_type: "backgroundColor".to_string(),
899 attrs: Some(serde_json::json!({"color": color})),
900 }
901 }
902
903 #[must_use]
905 pub fn subsup(kind: &str) -> Self {
906 Self {
907 mark_type: "subsup".to_string(),
908 attrs: Some(serde_json::json!({"type": kind})),
909 }
910 }
911
912 #[must_use]
914 pub fn alignment(align: &str) -> Self {
915 Self {
916 mark_type: "alignment".to_string(),
917 attrs: Some(serde_json::json!({"align": align})),
918 }
919 }
920
921 #[must_use]
923 pub fn indentation(level: u32) -> Self {
924 Self {
925 mark_type: "indentation".to_string(),
926 attrs: Some(serde_json::json!({"level": level})),
927 }
928 }
929
930 #[must_use]
932 pub fn breakout(mode: &str, width: Option<u32>) -> Self {
933 let mut attrs = serde_json::json!({"mode": mode});
934 if let Some(w) = width {
935 attrs["width"] = serde_json::json!(w);
936 }
937 Self {
938 mark_type: "breakout".to_string(),
939 attrs: Some(attrs),
940 }
941 }
942
943 #[must_use]
945 pub fn border(color: &str, size: u32) -> Self {
946 Self {
947 mark_type: "border".to_string(),
948 attrs: Some(serde_json::json!({"color": color, "size": size})),
949 }
950 }
951}
952
953#[cfg(test)]
954#[allow(clippy::unwrap_used, clippy::expect_used)]
955mod tests {
956 use super::*;
957
958 #[test]
959 fn empty_document_serialization() {
960 let doc = AdfDocument::new();
961 let json = serde_json::to_string(&doc).unwrap();
962 assert!(json.contains(r#""version":1"#));
963 assert!(json.contains(r#""type":"doc""#));
964 }
965
966 #[test]
967 fn document_with_paragraph() {
968 let doc = AdfDocument {
969 version: 1,
970 doc_type: "doc".to_string(),
971 content: vec![AdfNode::paragraph(vec![AdfNode::text("Hello world")])],
972 };
973 let json = serde_json::to_value(&doc).unwrap();
974 let content = json["content"][0].clone();
975 assert_eq!(content["type"], "paragraph");
976 assert_eq!(content["content"][0]["text"], "Hello world");
977 }
978
979 #[test]
980 fn text_with_marks() {
981 let node = AdfNode::text_with_marks("bold text", vec![AdfMark::strong()]);
982 let json = serde_json::to_value(&node).unwrap();
983 assert_eq!(json["marks"][0]["type"], "strong");
984 }
985
986 #[test]
987 fn heading_with_level() {
988 let node = AdfNode::heading(2, vec![AdfNode::text("Title")]);
989 let json = serde_json::to_value(&node).unwrap();
990 assert_eq!(json["attrs"]["level"], 2);
991 assert_eq!(json["content"][0]["text"], "Title");
992 }
993
994 #[test]
995 fn code_block_with_language() {
996 let node = AdfNode::code_block(Some("rust"), "fn main() {}");
997 let json = serde_json::to_value(&node).unwrap();
998 assert_eq!(json["attrs"]["language"], "rust");
999 assert_eq!(json["content"][0]["text"], "fn main() {}");
1000 }
1001
1002 #[test]
1003 fn link_mark_attributes() {
1004 let mark = AdfMark::link("https://example.com");
1005 let json = serde_json::to_value(&mark).unwrap();
1006 assert_eq!(json["attrs"]["href"], "https://example.com");
1007 }
1008
1009 #[test]
1010 fn real_jira_adf_deserialization() {
1011 let adf_json = r#"{
1012 "version": 1,
1013 "type": "doc",
1014 "content": [
1015 {
1016 "type": "paragraph",
1017 "content": [
1018 {"type": "text", "text": "Hello "},
1019 {"type": "text", "text": "world", "marks": [{"type": "strong"}]}
1020 ]
1021 },
1022 {
1023 "type": "heading",
1024 "attrs": {"level": 2},
1025 "content": [
1026 {"type": "text", "text": "Section"}
1027 ]
1028 }
1029 ]
1030 }"#;
1031
1032 let doc: AdfDocument = serde_json::from_str(adf_json).unwrap();
1033 assert_eq!(doc.version, 1);
1034 assert_eq!(doc.content.len(), 2);
1035 assert_eq!(doc.content[0].node_type, "paragraph");
1036 assert_eq!(doc.content[1].node_type, "heading");
1037 }
1038
1039 #[test]
1040 fn round_trip_serialization() {
1041 let doc = AdfDocument {
1042 version: 1,
1043 doc_type: "doc".to_string(),
1044 content: vec![
1045 AdfNode::heading(1, vec![AdfNode::text("Title")]),
1046 AdfNode::paragraph(vec![
1047 AdfNode::text("Normal "),
1048 AdfNode::text_with_marks("bold", vec![AdfMark::strong()]),
1049 AdfNode::text(" text"),
1050 ]),
1051 AdfNode::code_block(Some("rust"), "let x = 1;"),
1052 AdfNode::rule(),
1053 ],
1054 };
1055
1056 let json = serde_json::to_string(&doc).unwrap();
1057 let restored: AdfDocument = serde_json::from_str(&json).unwrap();
1058 assert_eq!(doc, restored);
1059 }
1060
1061 #[test]
1062 fn skip_none_fields_in_serialization() {
1063 let node = AdfNode::text("hello");
1064 let json = serde_json::to_value(&node).unwrap();
1065 assert!(json.get("attrs").is_none());
1066 assert!(json.get("content").is_none());
1067 assert!(json.get("marks").is_none());
1068 }
1069
1070 #[test]
1071 fn default_document() {
1072 let doc = AdfDocument::default();
1073 assert_eq!(doc.version, 1);
1074 assert_eq!(doc.doc_type, "doc");
1075 assert!(doc.content.is_empty());
1076 }
1077
1078 #[test]
1079 fn empty_paragraph_no_content() {
1080 let node = AdfNode::paragraph(vec![]);
1081 assert!(node.content.is_none());
1082 }
1083
1084 #[test]
1085 fn empty_heading_no_content() {
1086 let node = AdfNode::heading(1, vec![]);
1087 assert!(node.content.is_none());
1088 }
1089
1090 #[test]
1091 fn text_with_empty_marks_is_none() {
1092 let node = AdfNode::text_with_marks("test", vec![]);
1093 assert!(node.marks.is_none());
1094 }
1095
1096 #[test]
1097 fn code_block_no_language() {
1098 let node = AdfNode::code_block(None, "code");
1099 assert!(node.attrs.is_none());
1100 assert_eq!(
1101 node.content.as_ref().unwrap()[0].text.as_deref(),
1102 Some("code")
1103 );
1104 }
1105
1106 #[test]
1107 fn ordered_list_with_start() {
1108 let node = AdfNode::ordered_list(vec![], Some(5));
1109 let attrs = node.attrs.as_ref().unwrap();
1110 assert_eq!(attrs["order"], 5);
1111 }
1112
1113 #[test]
1114 fn ordered_list_no_start() {
1115 let node = AdfNode::ordered_list(vec![], None);
1116 assert!(node.attrs.is_none());
1117 }
1118
1119 #[test]
1120 fn media_single_with_alt() {
1121 let node = AdfNode::media_single("https://img.url", Some("Alt text"));
1122 let media = &node.content.as_ref().unwrap()[0];
1123 let attrs = media.attrs.as_ref().unwrap();
1124 assert_eq!(attrs["url"], "https://img.url");
1125 assert_eq!(attrs["alt"], "Alt text");
1126 }
1127
1128 #[test]
1129 fn media_single_no_alt() {
1130 let node = AdfNode::media_single("https://img.url", None);
1131 let media = &node.content.as_ref().unwrap()[0];
1132 let attrs = media.attrs.as_ref().unwrap();
1133 assert_eq!(attrs["url"], "https://img.url");
1134 assert!(attrs.get("alt").is_none());
1135 }
1136
1137 #[test]
1138 fn mark_constructors() {
1139 assert_eq!(AdfMark::em().mark_type, "em");
1140 assert_eq!(AdfMark::code().mark_type, "code");
1141 assert_eq!(AdfMark::strike().mark_type, "strike");
1142 }
1143
1144 #[test]
1145 fn table_structure() {
1146 let table = AdfNode::table(vec![AdfNode::table_row(vec![
1147 AdfNode::table_header(vec![AdfNode::paragraph(vec![AdfNode::text("H")])]),
1148 AdfNode::table_cell(vec![AdfNode::paragraph(vec![AdfNode::text("C")])]),
1149 ])]);
1150 assert_eq!(table.node_type, "table");
1151 let row = &table.content.as_ref().unwrap()[0];
1152 assert_eq!(row.node_type, "tableRow");
1153 let cells = row.content.as_ref().unwrap();
1154 assert_eq!(cells[0].node_type, "tableHeader");
1155 assert_eq!(cells[1].node_type, "tableCell");
1156 }
1157
1158 #[test]
1159 fn blockquote_structure() {
1160 let bq = AdfNode::blockquote(vec![AdfNode::paragraph(vec![AdfNode::text("quoted")])]);
1161 assert_eq!(bq.node_type, "blockquote");
1162 assert_eq!(bq.content.as_ref().unwrap()[0].node_type, "paragraph");
1163 }
1164
1165 #[test]
1166 fn hard_break_structure() {
1167 let br = AdfNode::hard_break();
1168 assert_eq!(br.node_type, "hardBreak");
1169 assert!(br.content.is_none());
1170 assert!(br.text.is_none());
1171 }
1172
1173 #[test]
1174 fn rule_structure() {
1175 let rule = AdfNode::rule();
1176 assert_eq!(rule.node_type, "rule");
1177 assert!(rule.content.is_none());
1178 }
1179
1180 #[test]
1183 fn bullet_list_structure() {
1184 let item = AdfNode::list_item(vec![AdfNode::paragraph(vec![AdfNode::text("item")])]);
1185 let list = AdfNode::bullet_list(vec![item]);
1186 assert_eq!(list.node_type, "bulletList");
1187 assert_eq!(list.content.as_ref().unwrap().len(), 1);
1188 }
1189
1190 #[test]
1191 fn list_item_structure() {
1192 let item = AdfNode::list_item(vec![AdfNode::paragraph(vec![AdfNode::text("text")])]);
1193 assert_eq!(item.node_type, "listItem");
1194 }
1195
1196 #[test]
1197 fn inline_card_structure() {
1198 let card = AdfNode::inline_card("https://example.com");
1199 assert_eq!(card.node_type, "inlineCard");
1200 assert_eq!(card.attrs.as_ref().unwrap()["url"], "https://example.com");
1201 }
1202
1203 #[test]
1204 fn task_list_structure() {
1205 let item = AdfNode::task_item("TODO", vec![AdfNode::text("do this")]);
1206 let list = AdfNode::task_list(vec![item]);
1207 assert_eq!(list.node_type, "taskList");
1208 assert!(list.attrs.as_ref().unwrap()["localId"].is_string());
1209 }
1210
1211 #[test]
1212 fn task_item_states() {
1213 let todo = AdfNode::task_item("TODO", vec![]);
1214 assert_eq!(todo.attrs.as_ref().unwrap()["state"], "TODO");
1215
1216 let done = AdfNode::task_item("DONE", vec![]);
1217 assert_eq!(done.attrs.as_ref().unwrap()["state"], "DONE");
1218 }
1219
1220 #[test]
1221 fn emoji_node() {
1222 let node = AdfNode::emoji(":thumbsup:");
1223 assert_eq!(node.node_type, "emoji");
1224 assert_eq!(node.attrs.as_ref().unwrap()["shortName"], ":thumbsup:");
1225 }
1226
1227 #[test]
1228 fn status_node() {
1229 let node = AdfNode::status("In Progress", "blue");
1230 assert_eq!(node.node_type, "status");
1231 assert_eq!(node.attrs.as_ref().unwrap()["text"], "In Progress");
1232 assert_eq!(node.attrs.as_ref().unwrap()["color"], "blue");
1233 }
1234
1235 #[test]
1236 fn date_node() {
1237 let node = AdfNode::date("1680307200000");
1238 assert_eq!(node.node_type, "date");
1239 assert_eq!(node.attrs.as_ref().unwrap()["timestamp"], "1680307200000");
1240 }
1241
1242 #[test]
1243 fn mention_node() {
1244 let node = AdfNode::mention("user-123", "Alice");
1245 assert_eq!(node.node_type, "mention");
1246 assert_eq!(node.attrs.as_ref().unwrap()["id"], "user-123");
1247 assert_eq!(node.attrs.as_ref().unwrap()["text"], "Alice");
1248 }
1249
1250 #[test]
1251 fn block_card_structure() {
1252 let card = AdfNode::block_card("https://example.com/page");
1253 assert_eq!(card.node_type, "blockCard");
1254 assert_eq!(
1255 card.attrs.as_ref().unwrap()["url"],
1256 "https://example.com/page"
1257 );
1258 }
1259
1260 #[test]
1261 fn embed_card_with_all_options() {
1262 let card = AdfNode::embed_card(
1263 "https://example.com",
1264 Some("wide"),
1265 Some(732.0),
1266 Some(100.0),
1267 );
1268 let attrs = card.attrs.as_ref().unwrap();
1269 assert_eq!(attrs["url"], "https://example.com");
1270 assert_eq!(attrs["layout"], "wide");
1271 assert_eq!(attrs["originalHeight"], 732.0);
1272 assert_eq!(attrs["width"], 100.0);
1273 }
1274
1275 #[test]
1276 fn embed_card_minimal() {
1277 let card = AdfNode::embed_card("https://example.com", None, None, None);
1278 let attrs = card.attrs.as_ref().unwrap();
1279 assert_eq!(attrs["url"], "https://example.com");
1280 assert!(attrs.get("layout").is_none());
1281 assert!(attrs.get("originalHeight").is_none());
1282 assert!(attrs.get("width").is_none());
1283 }
1284
1285 #[test]
1286 fn panel_structure() {
1287 let panel = AdfNode::panel(
1288 "info",
1289 vec![AdfNode::paragraph(vec![AdfNode::text("note")])],
1290 );
1291 assert_eq!(panel.node_type, "panel");
1292 assert_eq!(panel.attrs.as_ref().unwrap()["panelType"], "info");
1293 }
1294
1295 #[test]
1296 fn expand_with_title() {
1297 let node = AdfNode::expand(Some("Details"), vec![AdfNode::paragraph(vec![])]);
1298 assert_eq!(node.node_type, "expand");
1299 assert_eq!(node.attrs.as_ref().unwrap()["title"], "Details");
1300 }
1301
1302 #[test]
1303 fn expand_without_title() {
1304 let node = AdfNode::expand(None, vec![AdfNode::paragraph(vec![])]);
1305 assert_eq!(node.node_type, "expand");
1306 assert!(node.attrs.is_none());
1307 }
1308
1309 #[test]
1310 fn nested_expand_structure() {
1311 let node = AdfNode::nested_expand(Some("Inner"), vec![]);
1312 assert_eq!(node.node_type, "nestedExpand");
1313 assert_eq!(node.attrs.as_ref().unwrap()["title"], "Inner");
1314 }
1315
1316 #[test]
1317 fn layout_section_and_column() {
1318 let col = AdfNode::layout_column(50.0, vec![AdfNode::paragraph(vec![])]);
1319 assert_eq!(col.node_type, "layoutColumn");
1320 assert_eq!(col.attrs.as_ref().unwrap()["width"], 50.0);
1321
1322 let section = AdfNode::layout_section(vec![col]);
1323 assert_eq!(section.node_type, "layoutSection");
1324 }
1325
1326 #[test]
1327 fn decision_list_and_item() {
1328 let item = AdfNode::decision_item("DECIDED", vec![AdfNode::text("yes")]);
1329 assert_eq!(item.attrs.as_ref().unwrap()["state"], "DECIDED");
1330
1331 let list = AdfNode::decision_list(vec![item]);
1332 assert_eq!(list.node_type, "decisionList");
1333 }
1334
1335 #[test]
1336 fn extension_with_params() {
1337 let node = AdfNode::extension(
1338 "com.atlassian.jira",
1339 "issue-list",
1340 Some(serde_json::json!({"jql": "project = PROJ"})),
1341 );
1342 assert_eq!(node.node_type, "extension");
1343 let attrs = node.attrs.as_ref().unwrap();
1344 assert_eq!(attrs["extensionType"], "com.atlassian.jira");
1345 assert_eq!(attrs["parameters"]["jql"], "project = PROJ");
1346 }
1347
1348 #[test]
1349 fn extension_without_params() {
1350 let node = AdfNode::extension("com.atlassian.jira", "issue-list", None);
1351 let attrs = node.attrs.as_ref().unwrap();
1352 assert!(attrs.get("parameters").is_none());
1353 }
1354
1355 #[test]
1356 fn bodied_extension_structure() {
1357 let node = AdfNode::bodied_extension(
1358 "com.atlassian.jira",
1359 "issue-list",
1360 vec![AdfNode::paragraph(vec![AdfNode::text("body")])],
1361 );
1362 assert_eq!(node.node_type, "bodiedExtension");
1363 assert!(node.content.is_some());
1364 }
1365
1366 #[test]
1367 fn inline_extension_structure() {
1368 let node = AdfNode::inline_extension("com.test", "inline-key", Some("fallback"));
1369 assert_eq!(node.node_type, "inlineExtension");
1370 assert_eq!(node.text.as_deref(), Some("fallback"));
1371 }
1372
1373 #[test]
1374 fn inline_extension_no_fallback() {
1375 let node = AdfNode::inline_extension("com.test", "inline-key", None);
1376 assert!(node.text.is_none());
1377 }
1378
1379 #[test]
1380 fn table_with_attrs_structure() {
1381 let row = AdfNode::table_row(vec![]);
1382 let table = AdfNode::table_with_attrs(
1383 vec![row],
1384 serde_json::json!({"isNumberColumnEnabled": true, "layout": "default"}),
1385 );
1386 assert_eq!(table.node_type, "table");
1387 assert_eq!(table.attrs.as_ref().unwrap()["isNumberColumnEnabled"], true);
1388 }
1389
1390 #[test]
1391 fn table_header_with_attrs_structure() {
1392 let header = AdfNode::table_header_with_attrs(
1393 vec![AdfNode::paragraph(vec![AdfNode::text("H")])],
1394 serde_json::json!({"colspan": 2, "background": "#deebff"}),
1395 );
1396 assert_eq!(header.node_type, "tableHeader");
1397 assert_eq!(header.attrs.as_ref().unwrap()["colspan"], 2);
1398 }
1399
1400 #[test]
1401 fn table_cell_with_attrs_structure() {
1402 let cell = AdfNode::table_cell_with_attrs(
1403 vec![AdfNode::paragraph(vec![AdfNode::text("C")])],
1404 serde_json::json!({"rowspan": 3}),
1405 );
1406 assert_eq!(cell.node_type, "tableCell");
1407 assert_eq!(cell.attrs.as_ref().unwrap()["rowspan"], 3);
1408 }
1409
1410 #[test]
1413 fn underline_mark() {
1414 let mark = AdfMark::underline();
1415 assert_eq!(mark.mark_type, "underline");
1416 assert!(mark.attrs.is_none());
1417 }
1418
1419 #[test]
1420 fn text_color_mark() {
1421 let mark = AdfMark::text_color("#ff0000");
1422 assert_eq!(mark.mark_type, "textColor");
1423 assert_eq!(mark.attrs.as_ref().unwrap()["color"], "#ff0000");
1424 }
1425
1426 #[test]
1427 fn background_color_mark() {
1428 let mark = AdfMark::background_color("#00ff00");
1429 assert_eq!(mark.mark_type, "backgroundColor");
1430 assert_eq!(mark.attrs.as_ref().unwrap()["color"], "#00ff00");
1431 }
1432
1433 #[test]
1434 fn subsup_mark() {
1435 let mark = AdfMark::subsup("sub");
1436 assert_eq!(mark.mark_type, "subsup");
1437 assert_eq!(mark.attrs.as_ref().unwrap()["type"], "sub");
1438 }
1439
1440 #[test]
1441 fn alignment_mark() {
1442 let mark = AdfMark::alignment("center");
1443 assert_eq!(mark.mark_type, "alignment");
1444 assert_eq!(mark.attrs.as_ref().unwrap()["align"], "center");
1445 }
1446
1447 #[test]
1448 fn indentation_mark() {
1449 let mark = AdfMark::indentation(2);
1450 assert_eq!(mark.mark_type, "indentation");
1451 assert_eq!(mark.attrs.as_ref().unwrap()["level"], 2);
1452 }
1453
1454 #[test]
1455 fn breakout_mark() {
1456 let mark = AdfMark::breakout("wide", None);
1457 assert_eq!(mark.mark_type, "breakout");
1458 assert_eq!(mark.attrs.as_ref().unwrap()["mode"], "wide");
1459 assert!(mark.attrs.as_ref().unwrap().get("width").is_none());
1460 }
1461
1462 #[test]
1463 fn breakout_mark_with_width() {
1464 let mark = AdfMark::breakout("wide", Some(1200));
1465 assert_eq!(mark.mark_type, "breakout");
1466 assert_eq!(mark.attrs.as_ref().unwrap()["mode"], "wide");
1467 assert_eq!(mark.attrs.as_ref().unwrap()["width"], 1200);
1468 }
1469
1470 #[test]
1471 fn border_mark() {
1472 let mark = AdfMark::border("#ff000033", 2);
1473 assert_eq!(mark.mark_type, "border");
1474 let attrs = mark.attrs.as_ref().unwrap();
1475 assert_eq!(attrs["color"], "#ff000033");
1476 assert_eq!(attrs["size"], 2);
1477 }
1478
1479 #[test]
1480 fn table_cell_with_attrs_and_marks_builder() {
1481 let cell = AdfNode::table_cell_with_attrs_and_marks(
1482 vec![AdfNode::paragraph(vec![])],
1483 Some(serde_json::json!({"background": "#e6fcff"})),
1484 vec![AdfMark::border("#ff0000", 1)],
1485 );
1486 assert_eq!(cell.node_type, "tableCell");
1487 assert!(cell.marks.is_some());
1488 assert_eq!(cell.marks.as_ref().unwrap()[0].mark_type, "border");
1489 }
1490
1491 #[test]
1492 fn table_header_with_attrs_and_marks_builder() {
1493 let cell = AdfNode::table_header_with_attrs_and_marks(
1494 vec![AdfNode::paragraph(vec![])],
1495 None,
1496 vec![AdfMark::border("#0000ff", 3)],
1497 );
1498 assert_eq!(cell.node_type, "tableHeader");
1499 assert!(cell.marks.is_some());
1500 assert_eq!(cell.marks.as_ref().unwrap()[0].mark_type, "border");
1501 }
1502
1503 #[test]
1504 fn table_cell_with_empty_marks_has_none() {
1505 let cell = AdfNode::table_cell_with_attrs_and_marks(
1506 vec![AdfNode::paragraph(vec![])],
1507 None,
1508 vec![],
1509 );
1510 assert!(cell.marks.is_none(), "empty marks vec should become None");
1511 }
1512
1513 #[test]
1514 fn border_mark_serde_roundtrip() {
1515 let mark = AdfMark::border("#ff000033", 2);
1516 let json = serde_json::to_string(&mark).unwrap();
1517 let deserialized: AdfMark = serde_json::from_str(&json).unwrap();
1518 assert_eq!(deserialized.mark_type, "border");
1519 assert_eq!(deserialized.attrs.as_ref().unwrap()["color"], "#ff000033");
1520 assert_eq!(deserialized.attrs.as_ref().unwrap()["size"], 2);
1521 }
1522
1523 #[test]
1524 fn from_json_str_null_yields_empty_document() {
1525 let doc = AdfDocument::from_json_str("null").unwrap();
1526 assert_eq!(doc, AdfDocument::default());
1527 }
1528
1529 #[test]
1530 fn from_json_str_whitespace_null_yields_empty_document() {
1531 let doc = AdfDocument::from_json_str(" null\n").unwrap();
1532 assert_eq!(doc, AdfDocument::default());
1533 }
1534
1535 #[test]
1536 fn from_json_str_empty_doc_roundtrips() {
1537 let doc = AdfDocument::from_json_str(r#"{"version":1,"type":"doc","content":[]}"#).unwrap();
1538 assert_eq!(doc, AdfDocument::default());
1539 }
1540
1541 #[test]
1542 fn from_json_str_populated_doc() {
1543 let input = r#"{"version":1,"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hi"}]}]}"#;
1544 let doc = AdfDocument::from_json_str(input).unwrap();
1545 assert_eq!(doc.content.len(), 1);
1546 assert_eq!(doc.content[0].node_type, "paragraph");
1547 }
1548
1549 #[test]
1550 fn from_json_str_invalid_json_errors() {
1551 assert!(AdfDocument::from_json_str("not json").is_err());
1552 }
1553
1554 #[test]
1555 fn from_json_str_wrong_shape_errors() {
1556 assert!(AdfDocument::from_json_str(r#"{"foo":"bar"}"#).is_err());
1557 }
1558}