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