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
67impl AdfNode {
68 #[must_use]
70 pub fn text(content: &str) -> Self {
71 Self {
72 node_type: "text".to_string(),
73 attrs: None,
74 content: None,
75 text: Some(content.to_string()),
76 marks: None,
77 }
78 }
79
80 #[must_use]
82 pub fn text_with_marks(content: &str, marks: Vec<AdfMark>) -> Self {
83 Self {
84 node_type: "text".to_string(),
85 attrs: None,
86 content: None,
87 text: Some(content.to_string()),
88 marks: if marks.is_empty() { None } else { Some(marks) },
89 }
90 }
91
92 #[must_use]
94 pub fn paragraph(content: Vec<Self>) -> Self {
95 Self {
96 node_type: "paragraph".to_string(),
97 attrs: None,
98 content: if content.is_empty() {
99 None
100 } else {
101 Some(content)
102 },
103 text: None,
104 marks: None,
105 }
106 }
107
108 #[must_use]
110 pub fn heading(level: u8, content: Vec<Self>) -> Self {
111 Self {
112 node_type: "heading".to_string(),
113 attrs: Some(serde_json::json!({"level": level})),
114 content: if content.is_empty() {
115 None
116 } else {
117 Some(content)
118 },
119 text: None,
120 marks: None,
121 }
122 }
123
124 #[must_use]
126 pub fn code_block(language: Option<&str>, text: &str) -> Self {
127 Self {
128 node_type: "codeBlock".to_string(),
129 attrs: language.map(|lang| serde_json::json!({"language": lang})),
130 content: Some(vec![Self::text(text)]),
131 text: None,
132 marks: None,
133 }
134 }
135
136 #[must_use]
138 pub fn blockquote(content: Vec<Self>) -> Self {
139 Self {
140 node_type: "blockquote".to_string(),
141 attrs: None,
142 content: Some(content),
143 text: None,
144 marks: None,
145 }
146 }
147
148 #[must_use]
150 pub fn rule() -> Self {
151 Self {
152 node_type: "rule".to_string(),
153 attrs: None,
154 content: None,
155 text: None,
156 marks: None,
157 }
158 }
159
160 #[must_use]
162 pub fn bullet_list(items: Vec<Self>) -> Self {
163 Self {
164 node_type: "bulletList".to_string(),
165 attrs: None,
166 content: Some(items),
167 text: None,
168 marks: None,
169 }
170 }
171
172 #[must_use]
174 pub fn ordered_list(items: Vec<Self>, start: Option<u32>) -> Self {
175 Self {
176 node_type: "orderedList".to_string(),
177 attrs: start.map(|s| serde_json::json!({"order": s})),
178 content: Some(items),
179 text: None,
180 marks: None,
181 }
182 }
183
184 #[must_use]
186 pub fn list_item(content: Vec<Self>) -> Self {
187 Self {
188 node_type: "listItem".to_string(),
189 attrs: None,
190 content: Some(content),
191 text: None,
192 marks: None,
193 }
194 }
195
196 #[must_use]
198 pub fn hard_break() -> Self {
199 Self {
200 node_type: "hardBreak".to_string(),
201 attrs: None,
202 content: None,
203 text: None,
204 marks: None,
205 }
206 }
207
208 #[must_use]
210 pub fn table(rows: Vec<Self>) -> Self {
211 Self {
212 node_type: "table".to_string(),
213 attrs: None,
214 content: Some(rows),
215 text: None,
216 marks: None,
217 }
218 }
219
220 #[must_use]
222 pub fn table_with_attrs(rows: Vec<Self>, attrs: serde_json::Value) -> Self {
223 Self {
224 node_type: "table".to_string(),
225 attrs: Some(attrs),
226 content: Some(rows),
227 text: None,
228 marks: None,
229 }
230 }
231
232 #[must_use]
234 pub fn table_row(cells: Vec<Self>) -> Self {
235 Self {
236 node_type: "tableRow".to_string(),
237 attrs: None,
238 content: Some(cells),
239 text: None,
240 marks: None,
241 }
242 }
243
244 #[must_use]
246 pub fn table_header(content: Vec<Self>) -> Self {
247 Self {
248 node_type: "tableHeader".to_string(),
249 attrs: None,
250 content: Some(content),
251 text: None,
252 marks: None,
253 }
254 }
255
256 #[must_use]
258 pub fn table_header_with_attrs(content: Vec<Self>, attrs: serde_json::Value) -> Self {
259 Self {
260 node_type: "tableHeader".to_string(),
261 attrs: Some(attrs),
262 content: Some(content),
263 text: None,
264 marks: None,
265 }
266 }
267
268 #[must_use]
270 pub fn table_cell(content: Vec<Self>) -> Self {
271 Self {
272 node_type: "tableCell".to_string(),
273 attrs: None,
274 content: Some(content),
275 text: None,
276 marks: None,
277 }
278 }
279
280 #[must_use]
282 pub fn table_cell_with_attrs(content: Vec<Self>, attrs: serde_json::Value) -> Self {
283 Self {
284 node_type: "tableCell".to_string(),
285 attrs: Some(attrs),
286 content: Some(content),
287 text: None,
288 marks: None,
289 }
290 }
291
292 #[must_use]
294 pub fn inline_card(url: &str) -> Self {
295 Self {
296 node_type: "inlineCard".to_string(),
297 attrs: Some(serde_json::json!({"url": url})),
298 content: None,
299 text: None,
300 marks: None,
301 }
302 }
303
304 #[must_use]
306 pub fn media_single(url: &str, alt: Option<&str>) -> Self {
307 let mut media_attrs = serde_json::json!({
308 "type": "external",
309 "url": url,
310 });
311 if let Some(alt_text) = alt {
312 media_attrs["alt"] = serde_json::Value::String(alt_text.to_string());
313 }
314 Self {
315 node_type: "mediaSingle".to_string(),
316 attrs: Some(serde_json::json!({"layout": "center"})),
317 content: Some(vec![Self {
318 node_type: "media".to_string(),
319 attrs: Some(media_attrs),
320 content: None,
321 text: None,
322 marks: None,
323 }]),
324 text: None,
325 marks: None,
326 }
327 }
328
329 #[must_use]
333 pub fn task_list(items: Vec<Self>) -> Self {
334 Self {
335 node_type: "taskList".to_string(),
336 attrs: Some(serde_json::json!({"localId": uuid_placeholder()})),
337 content: Some(items),
338 text: None,
339 marks: None,
340 }
341 }
342
343 #[must_use]
345 pub fn task_item(state: &str, content: Vec<Self>) -> Self {
346 Self {
347 node_type: "taskItem".to_string(),
348 attrs: Some(serde_json::json!({
349 "localId": uuid_placeholder(),
350 "state": state,
351 })),
352 content: Some(content),
353 text: None,
354 marks: None,
355 }
356 }
357
358 #[must_use]
362 pub fn emoji(short_name: &str) -> Self {
363 Self {
364 node_type: "emoji".to_string(),
365 attrs: Some(serde_json::json!({"shortName": short_name})),
366 content: None,
367 text: None,
368 marks: None,
369 }
370 }
371
372 #[must_use]
374 pub fn status(text: &str, color: &str) -> Self {
375 Self {
376 node_type: "status".to_string(),
377 attrs: Some(serde_json::json!({
378 "text": text,
379 "color": color,
380 "localId": uuid_placeholder(),
381 })),
382 content: None,
383 text: None,
384 marks: None,
385 }
386 }
387
388 #[must_use]
390 pub fn date(timestamp: &str) -> Self {
391 Self {
392 node_type: "date".to_string(),
393 attrs: Some(serde_json::json!({"timestamp": timestamp})),
394 content: None,
395 text: None,
396 marks: None,
397 }
398 }
399
400 #[must_use]
402 pub fn mention(id: &str, display_text: &str) -> Self {
403 Self {
404 node_type: "mention".to_string(),
405 attrs: Some(serde_json::json!({
406 "id": id,
407 "text": display_text,
408 })),
409 content: None,
410 text: None,
411 marks: None,
412 }
413 }
414
415 #[must_use]
419 pub fn block_card(url: &str) -> Self {
420 Self {
421 node_type: "blockCard".to_string(),
422 attrs: Some(serde_json::json!({"url": url})),
423 content: None,
424 text: None,
425 marks: None,
426 }
427 }
428
429 #[must_use]
431 pub fn embed_card(url: &str, layout: Option<&str>, width: Option<u32>) -> Self {
432 let mut attrs = serde_json::json!({"url": url});
433 if let Some(l) = layout {
434 attrs["layout"] = serde_json::Value::String(l.to_string());
435 }
436 if let Some(w) = width {
437 attrs["width"] = serde_json::json!(w);
438 }
439 Self {
440 node_type: "embedCard".to_string(),
441 attrs: Some(attrs),
442 content: None,
443 text: None,
444 marks: None,
445 }
446 }
447
448 #[must_use]
452 pub fn panel(panel_type: &str, content: Vec<Self>) -> Self {
453 Self {
454 node_type: "panel".to_string(),
455 attrs: Some(serde_json::json!({"panelType": panel_type})),
456 content: Some(content),
457 text: None,
458 marks: None,
459 }
460 }
461
462 #[must_use]
464 pub fn expand(title: Option<&str>, content: Vec<Self>) -> Self {
465 let attrs = title.map(|t| serde_json::json!({"title": t}));
466 Self {
467 node_type: "expand".to_string(),
468 attrs,
469 content: Some(content),
470 text: None,
471 marks: None,
472 }
473 }
474
475 #[must_use]
477 pub fn nested_expand(title: Option<&str>, content: Vec<Self>) -> Self {
478 let attrs = title.map(|t| serde_json::json!({"title": t}));
479 Self {
480 node_type: "nestedExpand".to_string(),
481 attrs,
482 content: Some(content),
483 text: None,
484 marks: None,
485 }
486 }
487
488 #[must_use]
492 pub fn layout_section(columns: Vec<Self>) -> Self {
493 Self {
494 node_type: "layoutSection".to_string(),
495 attrs: None,
496 content: Some(columns),
497 text: None,
498 marks: None,
499 }
500 }
501
502 #[must_use]
504 pub fn layout_column(width: f64, content: Vec<Self>) -> Self {
505 Self {
506 node_type: "layoutColumn".to_string(),
507 attrs: Some(serde_json::json!({"width": width})),
508 content: Some(content),
509 text: None,
510 marks: None,
511 }
512 }
513
514 #[must_use]
518 pub fn decision_list(items: Vec<Self>) -> Self {
519 Self {
520 node_type: "decisionList".to_string(),
521 attrs: Some(serde_json::json!({"localId": uuid_placeholder()})),
522 content: Some(items),
523 text: None,
524 marks: None,
525 }
526 }
527
528 #[must_use]
530 pub fn decision_item(state: &str, content: Vec<Self>) -> Self {
531 Self {
532 node_type: "decisionItem".to_string(),
533 attrs: Some(serde_json::json!({
534 "localId": uuid_placeholder(),
535 "state": state,
536 })),
537 content: Some(content),
538 text: None,
539 marks: None,
540 }
541 }
542
543 #[must_use]
547 pub fn extension(
548 extension_type: &str,
549 extension_key: &str,
550 params: Option<serde_json::Value>,
551 ) -> Self {
552 let mut attrs = serde_json::json!({
553 "extensionType": extension_type,
554 "extensionKey": extension_key,
555 });
556 if let Some(p) = params {
557 attrs["parameters"] = p;
558 }
559 Self {
560 node_type: "extension".to_string(),
561 attrs: Some(attrs),
562 content: None,
563 text: None,
564 marks: None,
565 }
566 }
567
568 #[must_use]
570 pub fn bodied_extension(extension_type: &str, extension_key: &str, content: Vec<Self>) -> Self {
571 Self {
572 node_type: "bodiedExtension".to_string(),
573 attrs: Some(serde_json::json!({
574 "extensionType": extension_type,
575 "extensionKey": extension_key,
576 })),
577 content: Some(content),
578 text: None,
579 marks: None,
580 }
581 }
582
583 #[must_use]
585 pub fn inline_extension(
586 extension_type: &str,
587 extension_key: &str,
588 fallback_text: Option<&str>,
589 ) -> Self {
590 Self {
591 node_type: "inlineExtension".to_string(),
592 attrs: Some(serde_json::json!({
593 "extensionType": extension_type,
594 "extensionKey": extension_key,
595 })),
596 content: None,
597 text: fallback_text.map(String::from),
598 marks: None,
599 }
600 }
601}
602
603fn uuid_placeholder() -> String {
607 "00000000-0000-0000-0000-000000000000".to_string()
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
612pub struct AdfMark {
613 #[serde(rename = "type")]
615 pub mark_type: String,
616
617 #[serde(skip_serializing_if = "Option::is_none")]
619 pub attrs: Option<serde_json::Value>,
620}
621
622impl AdfMark {
623 #[must_use]
625 pub fn strong() -> Self {
626 Self {
627 mark_type: "strong".to_string(),
628 attrs: None,
629 }
630 }
631
632 #[must_use]
634 pub fn em() -> Self {
635 Self {
636 mark_type: "em".to_string(),
637 attrs: None,
638 }
639 }
640
641 #[must_use]
643 pub fn code() -> Self {
644 Self {
645 mark_type: "code".to_string(),
646 attrs: None,
647 }
648 }
649
650 #[must_use]
652 pub fn strike() -> Self {
653 Self {
654 mark_type: "strike".to_string(),
655 attrs: None,
656 }
657 }
658
659 #[must_use]
661 pub fn link(href: &str) -> Self {
662 Self {
663 mark_type: "link".to_string(),
664 attrs: Some(serde_json::json!({"href": href})),
665 }
666 }
667
668 #[must_use]
670 pub fn underline() -> Self {
671 Self {
672 mark_type: "underline".to_string(),
673 attrs: None,
674 }
675 }
676
677 #[must_use]
679 pub fn text_color(color: &str) -> Self {
680 Self {
681 mark_type: "textColor".to_string(),
682 attrs: Some(serde_json::json!({"color": color})),
683 }
684 }
685
686 #[must_use]
688 pub fn background_color(color: &str) -> Self {
689 Self {
690 mark_type: "backgroundColor".to_string(),
691 attrs: Some(serde_json::json!({"color": color})),
692 }
693 }
694
695 #[must_use]
697 pub fn subsup(kind: &str) -> Self {
698 Self {
699 mark_type: "subsup".to_string(),
700 attrs: Some(serde_json::json!({"type": kind})),
701 }
702 }
703
704 #[must_use]
706 pub fn alignment(align: &str) -> Self {
707 Self {
708 mark_type: "alignment".to_string(),
709 attrs: Some(serde_json::json!({"align": align})),
710 }
711 }
712
713 #[must_use]
715 pub fn indentation(level: u32) -> Self {
716 Self {
717 mark_type: "indentation".to_string(),
718 attrs: Some(serde_json::json!({"level": level})),
719 }
720 }
721
722 #[must_use]
724 pub fn breakout(mode: &str) -> Self {
725 Self {
726 mark_type: "breakout".to_string(),
727 attrs: Some(serde_json::json!({"mode": mode})),
728 }
729 }
730}
731
732#[cfg(test)]
733#[allow(clippy::unwrap_used, clippy::expect_used)]
734mod tests {
735 use super::*;
736
737 #[test]
738 fn empty_document_serialization() {
739 let doc = AdfDocument::new();
740 let json = serde_json::to_string(&doc).unwrap();
741 assert!(json.contains(r#""version":1"#));
742 assert!(json.contains(r#""type":"doc""#));
743 }
744
745 #[test]
746 fn document_with_paragraph() {
747 let doc = AdfDocument {
748 version: 1,
749 doc_type: "doc".to_string(),
750 content: vec![AdfNode::paragraph(vec![AdfNode::text("Hello world")])],
751 };
752 let json = serde_json::to_value(&doc).unwrap();
753 let content = json["content"][0].clone();
754 assert_eq!(content["type"], "paragraph");
755 assert_eq!(content["content"][0]["text"], "Hello world");
756 }
757
758 #[test]
759 fn text_with_marks() {
760 let node = AdfNode::text_with_marks("bold text", vec![AdfMark::strong()]);
761 let json = serde_json::to_value(&node).unwrap();
762 assert_eq!(json["marks"][0]["type"], "strong");
763 }
764
765 #[test]
766 fn heading_with_level() {
767 let node = AdfNode::heading(2, vec![AdfNode::text("Title")]);
768 let json = serde_json::to_value(&node).unwrap();
769 assert_eq!(json["attrs"]["level"], 2);
770 assert_eq!(json["content"][0]["text"], "Title");
771 }
772
773 #[test]
774 fn code_block_with_language() {
775 let node = AdfNode::code_block(Some("rust"), "fn main() {}");
776 let json = serde_json::to_value(&node).unwrap();
777 assert_eq!(json["attrs"]["language"], "rust");
778 assert_eq!(json["content"][0]["text"], "fn main() {}");
779 }
780
781 #[test]
782 fn link_mark_attributes() {
783 let mark = AdfMark::link("https://example.com");
784 let json = serde_json::to_value(&mark).unwrap();
785 assert_eq!(json["attrs"]["href"], "https://example.com");
786 }
787
788 #[test]
789 fn real_jira_adf_deserialization() {
790 let adf_json = r#"{
791 "version": 1,
792 "type": "doc",
793 "content": [
794 {
795 "type": "paragraph",
796 "content": [
797 {"type": "text", "text": "Hello "},
798 {"type": "text", "text": "world", "marks": [{"type": "strong"}]}
799 ]
800 },
801 {
802 "type": "heading",
803 "attrs": {"level": 2},
804 "content": [
805 {"type": "text", "text": "Section"}
806 ]
807 }
808 ]
809 }"#;
810
811 let doc: AdfDocument = serde_json::from_str(adf_json).unwrap();
812 assert_eq!(doc.version, 1);
813 assert_eq!(doc.content.len(), 2);
814 assert_eq!(doc.content[0].node_type, "paragraph");
815 assert_eq!(doc.content[1].node_type, "heading");
816 }
817
818 #[test]
819 fn round_trip_serialization() {
820 let doc = AdfDocument {
821 version: 1,
822 doc_type: "doc".to_string(),
823 content: vec![
824 AdfNode::heading(1, vec![AdfNode::text("Title")]),
825 AdfNode::paragraph(vec![
826 AdfNode::text("Normal "),
827 AdfNode::text_with_marks("bold", vec![AdfMark::strong()]),
828 AdfNode::text(" text"),
829 ]),
830 AdfNode::code_block(Some("rust"), "let x = 1;"),
831 AdfNode::rule(),
832 ],
833 };
834
835 let json = serde_json::to_string(&doc).unwrap();
836 let restored: AdfDocument = serde_json::from_str(&json).unwrap();
837 assert_eq!(doc, restored);
838 }
839
840 #[test]
841 fn skip_none_fields_in_serialization() {
842 let node = AdfNode::text("hello");
843 let json = serde_json::to_value(&node).unwrap();
844 assert!(json.get("attrs").is_none());
845 assert!(json.get("content").is_none());
846 assert!(json.get("marks").is_none());
847 }
848
849 #[test]
850 fn default_document() {
851 let doc = AdfDocument::default();
852 assert_eq!(doc.version, 1);
853 assert_eq!(doc.doc_type, "doc");
854 assert!(doc.content.is_empty());
855 }
856
857 #[test]
858 fn empty_paragraph_no_content() {
859 let node = AdfNode::paragraph(vec![]);
860 assert!(node.content.is_none());
861 }
862
863 #[test]
864 fn empty_heading_no_content() {
865 let node = AdfNode::heading(1, vec![]);
866 assert!(node.content.is_none());
867 }
868
869 #[test]
870 fn text_with_empty_marks_is_none() {
871 let node = AdfNode::text_with_marks("test", vec![]);
872 assert!(node.marks.is_none());
873 }
874
875 #[test]
876 fn code_block_no_language() {
877 let node = AdfNode::code_block(None, "code");
878 assert!(node.attrs.is_none());
879 assert_eq!(
880 node.content.as_ref().unwrap()[0].text.as_deref(),
881 Some("code")
882 );
883 }
884
885 #[test]
886 fn ordered_list_with_start() {
887 let node = AdfNode::ordered_list(vec![], Some(5));
888 let attrs = node.attrs.as_ref().unwrap();
889 assert_eq!(attrs["order"], 5);
890 }
891
892 #[test]
893 fn ordered_list_no_start() {
894 let node = AdfNode::ordered_list(vec![], None);
895 assert!(node.attrs.is_none());
896 }
897
898 #[test]
899 fn media_single_with_alt() {
900 let node = AdfNode::media_single("https://img.url", Some("Alt text"));
901 let media = &node.content.as_ref().unwrap()[0];
902 let attrs = media.attrs.as_ref().unwrap();
903 assert_eq!(attrs["url"], "https://img.url");
904 assert_eq!(attrs["alt"], "Alt text");
905 }
906
907 #[test]
908 fn media_single_no_alt() {
909 let node = AdfNode::media_single("https://img.url", None);
910 let media = &node.content.as_ref().unwrap()[0];
911 let attrs = media.attrs.as_ref().unwrap();
912 assert_eq!(attrs["url"], "https://img.url");
913 assert!(attrs.get("alt").is_none());
914 }
915
916 #[test]
917 fn mark_constructors() {
918 assert_eq!(AdfMark::em().mark_type, "em");
919 assert_eq!(AdfMark::code().mark_type, "code");
920 assert_eq!(AdfMark::strike().mark_type, "strike");
921 }
922
923 #[test]
924 fn table_structure() {
925 let table = AdfNode::table(vec![AdfNode::table_row(vec![
926 AdfNode::table_header(vec![AdfNode::paragraph(vec![AdfNode::text("H")])]),
927 AdfNode::table_cell(vec![AdfNode::paragraph(vec![AdfNode::text("C")])]),
928 ])]);
929 assert_eq!(table.node_type, "table");
930 let row = &table.content.as_ref().unwrap()[0];
931 assert_eq!(row.node_type, "tableRow");
932 let cells = row.content.as_ref().unwrap();
933 assert_eq!(cells[0].node_type, "tableHeader");
934 assert_eq!(cells[1].node_type, "tableCell");
935 }
936
937 #[test]
938 fn blockquote_structure() {
939 let bq = AdfNode::blockquote(vec![AdfNode::paragraph(vec![AdfNode::text("quoted")])]);
940 assert_eq!(bq.node_type, "blockquote");
941 assert_eq!(bq.content.as_ref().unwrap()[0].node_type, "paragraph");
942 }
943
944 #[test]
945 fn hard_break_structure() {
946 let br = AdfNode::hard_break();
947 assert_eq!(br.node_type, "hardBreak");
948 assert!(br.content.is_none());
949 assert!(br.text.is_none());
950 }
951
952 #[test]
953 fn rule_structure() {
954 let rule = AdfNode::rule();
955 assert_eq!(rule.node_type, "rule");
956 assert!(rule.content.is_none());
957 }
958
959 #[test]
962 fn bullet_list_structure() {
963 let item = AdfNode::list_item(vec![AdfNode::paragraph(vec![AdfNode::text("item")])]);
964 let list = AdfNode::bullet_list(vec![item]);
965 assert_eq!(list.node_type, "bulletList");
966 assert_eq!(list.content.as_ref().unwrap().len(), 1);
967 }
968
969 #[test]
970 fn list_item_structure() {
971 let item = AdfNode::list_item(vec![AdfNode::paragraph(vec![AdfNode::text("text")])]);
972 assert_eq!(item.node_type, "listItem");
973 }
974
975 #[test]
976 fn inline_card_structure() {
977 let card = AdfNode::inline_card("https://example.com");
978 assert_eq!(card.node_type, "inlineCard");
979 assert_eq!(card.attrs.as_ref().unwrap()["url"], "https://example.com");
980 }
981
982 #[test]
983 fn task_list_structure() {
984 let item = AdfNode::task_item("TODO", vec![AdfNode::text("do this")]);
985 let list = AdfNode::task_list(vec![item]);
986 assert_eq!(list.node_type, "taskList");
987 assert!(list.attrs.as_ref().unwrap()["localId"].is_string());
988 }
989
990 #[test]
991 fn task_item_states() {
992 let todo = AdfNode::task_item("TODO", vec![]);
993 assert_eq!(todo.attrs.as_ref().unwrap()["state"], "TODO");
994
995 let done = AdfNode::task_item("DONE", vec![]);
996 assert_eq!(done.attrs.as_ref().unwrap()["state"], "DONE");
997 }
998
999 #[test]
1000 fn emoji_node() {
1001 let node = AdfNode::emoji(":thumbsup:");
1002 assert_eq!(node.node_type, "emoji");
1003 assert_eq!(node.attrs.as_ref().unwrap()["shortName"], ":thumbsup:");
1004 }
1005
1006 #[test]
1007 fn status_node() {
1008 let node = AdfNode::status("In Progress", "blue");
1009 assert_eq!(node.node_type, "status");
1010 assert_eq!(node.attrs.as_ref().unwrap()["text"], "In Progress");
1011 assert_eq!(node.attrs.as_ref().unwrap()["color"], "blue");
1012 }
1013
1014 #[test]
1015 fn date_node() {
1016 let node = AdfNode::date("1680307200000");
1017 assert_eq!(node.node_type, "date");
1018 assert_eq!(node.attrs.as_ref().unwrap()["timestamp"], "1680307200000");
1019 }
1020
1021 #[test]
1022 fn mention_node() {
1023 let node = AdfNode::mention("user-123", "Alice");
1024 assert_eq!(node.node_type, "mention");
1025 assert_eq!(node.attrs.as_ref().unwrap()["id"], "user-123");
1026 assert_eq!(node.attrs.as_ref().unwrap()["text"], "Alice");
1027 }
1028
1029 #[test]
1030 fn block_card_structure() {
1031 let card = AdfNode::block_card("https://example.com/page");
1032 assert_eq!(card.node_type, "blockCard");
1033 assert_eq!(
1034 card.attrs.as_ref().unwrap()["url"],
1035 "https://example.com/page"
1036 );
1037 }
1038
1039 #[test]
1040 fn embed_card_with_all_options() {
1041 let card = AdfNode::embed_card("https://example.com", Some("wide"), Some(800));
1042 let attrs = card.attrs.as_ref().unwrap();
1043 assert_eq!(attrs["url"], "https://example.com");
1044 assert_eq!(attrs["layout"], "wide");
1045 assert_eq!(attrs["width"], 800);
1046 }
1047
1048 #[test]
1049 fn embed_card_minimal() {
1050 let card = AdfNode::embed_card("https://example.com", None, None);
1051 let attrs = card.attrs.as_ref().unwrap();
1052 assert_eq!(attrs["url"], "https://example.com");
1053 assert!(attrs.get("layout").is_none());
1054 assert!(attrs.get("width").is_none());
1055 }
1056
1057 #[test]
1058 fn panel_structure() {
1059 let panel = AdfNode::panel(
1060 "info",
1061 vec![AdfNode::paragraph(vec![AdfNode::text("note")])],
1062 );
1063 assert_eq!(panel.node_type, "panel");
1064 assert_eq!(panel.attrs.as_ref().unwrap()["panelType"], "info");
1065 }
1066
1067 #[test]
1068 fn expand_with_title() {
1069 let node = AdfNode::expand(Some("Details"), vec![AdfNode::paragraph(vec![])]);
1070 assert_eq!(node.node_type, "expand");
1071 assert_eq!(node.attrs.as_ref().unwrap()["title"], "Details");
1072 }
1073
1074 #[test]
1075 fn expand_without_title() {
1076 let node = AdfNode::expand(None, vec![AdfNode::paragraph(vec![])]);
1077 assert_eq!(node.node_type, "expand");
1078 assert!(node.attrs.is_none());
1079 }
1080
1081 #[test]
1082 fn nested_expand_structure() {
1083 let node = AdfNode::nested_expand(Some("Inner"), vec![]);
1084 assert_eq!(node.node_type, "nestedExpand");
1085 assert_eq!(node.attrs.as_ref().unwrap()["title"], "Inner");
1086 }
1087
1088 #[test]
1089 fn layout_section_and_column() {
1090 let col = AdfNode::layout_column(50.0, vec![AdfNode::paragraph(vec![])]);
1091 assert_eq!(col.node_type, "layoutColumn");
1092 assert_eq!(col.attrs.as_ref().unwrap()["width"], 50.0);
1093
1094 let section = AdfNode::layout_section(vec![col]);
1095 assert_eq!(section.node_type, "layoutSection");
1096 }
1097
1098 #[test]
1099 fn decision_list_and_item() {
1100 let item = AdfNode::decision_item("DECIDED", vec![AdfNode::text("yes")]);
1101 assert_eq!(item.attrs.as_ref().unwrap()["state"], "DECIDED");
1102
1103 let list = AdfNode::decision_list(vec![item]);
1104 assert_eq!(list.node_type, "decisionList");
1105 }
1106
1107 #[test]
1108 fn extension_with_params() {
1109 let node = AdfNode::extension(
1110 "com.atlassian.jira",
1111 "issue-list",
1112 Some(serde_json::json!({"jql": "project = PROJ"})),
1113 );
1114 assert_eq!(node.node_type, "extension");
1115 let attrs = node.attrs.as_ref().unwrap();
1116 assert_eq!(attrs["extensionType"], "com.atlassian.jira");
1117 assert_eq!(attrs["parameters"]["jql"], "project = PROJ");
1118 }
1119
1120 #[test]
1121 fn extension_without_params() {
1122 let node = AdfNode::extension("com.atlassian.jira", "issue-list", None);
1123 let attrs = node.attrs.as_ref().unwrap();
1124 assert!(attrs.get("parameters").is_none());
1125 }
1126
1127 #[test]
1128 fn bodied_extension_structure() {
1129 let node = AdfNode::bodied_extension(
1130 "com.atlassian.jira",
1131 "issue-list",
1132 vec![AdfNode::paragraph(vec![AdfNode::text("body")])],
1133 );
1134 assert_eq!(node.node_type, "bodiedExtension");
1135 assert!(node.content.is_some());
1136 }
1137
1138 #[test]
1139 fn inline_extension_structure() {
1140 let node = AdfNode::inline_extension("com.test", "inline-key", Some("fallback"));
1141 assert_eq!(node.node_type, "inlineExtension");
1142 assert_eq!(node.text.as_deref(), Some("fallback"));
1143 }
1144
1145 #[test]
1146 fn inline_extension_no_fallback() {
1147 let node = AdfNode::inline_extension("com.test", "inline-key", None);
1148 assert!(node.text.is_none());
1149 }
1150
1151 #[test]
1152 fn table_with_attrs_structure() {
1153 let row = AdfNode::table_row(vec![]);
1154 let table = AdfNode::table_with_attrs(
1155 vec![row],
1156 serde_json::json!({"isNumberColumnEnabled": true, "layout": "default"}),
1157 );
1158 assert_eq!(table.node_type, "table");
1159 assert_eq!(table.attrs.as_ref().unwrap()["isNumberColumnEnabled"], true);
1160 }
1161
1162 #[test]
1163 fn table_header_with_attrs_structure() {
1164 let header = AdfNode::table_header_with_attrs(
1165 vec![AdfNode::paragraph(vec![AdfNode::text("H")])],
1166 serde_json::json!({"colspan": 2, "background": "#deebff"}),
1167 );
1168 assert_eq!(header.node_type, "tableHeader");
1169 assert_eq!(header.attrs.as_ref().unwrap()["colspan"], 2);
1170 }
1171
1172 #[test]
1173 fn table_cell_with_attrs_structure() {
1174 let cell = AdfNode::table_cell_with_attrs(
1175 vec![AdfNode::paragraph(vec![AdfNode::text("C")])],
1176 serde_json::json!({"rowspan": 3}),
1177 );
1178 assert_eq!(cell.node_type, "tableCell");
1179 assert_eq!(cell.attrs.as_ref().unwrap()["rowspan"], 3);
1180 }
1181
1182 #[test]
1185 fn underline_mark() {
1186 let mark = AdfMark::underline();
1187 assert_eq!(mark.mark_type, "underline");
1188 assert!(mark.attrs.is_none());
1189 }
1190
1191 #[test]
1192 fn text_color_mark() {
1193 let mark = AdfMark::text_color("#ff0000");
1194 assert_eq!(mark.mark_type, "textColor");
1195 assert_eq!(mark.attrs.as_ref().unwrap()["color"], "#ff0000");
1196 }
1197
1198 #[test]
1199 fn background_color_mark() {
1200 let mark = AdfMark::background_color("#00ff00");
1201 assert_eq!(mark.mark_type, "backgroundColor");
1202 assert_eq!(mark.attrs.as_ref().unwrap()["color"], "#00ff00");
1203 }
1204
1205 #[test]
1206 fn subsup_mark() {
1207 let mark = AdfMark::subsup("sub");
1208 assert_eq!(mark.mark_type, "subsup");
1209 assert_eq!(mark.attrs.as_ref().unwrap()["type"], "sub");
1210 }
1211
1212 #[test]
1213 fn alignment_mark() {
1214 let mark = AdfMark::alignment("center");
1215 assert_eq!(mark.mark_type, "alignment");
1216 assert_eq!(mark.attrs.as_ref().unwrap()["align"], "center");
1217 }
1218
1219 #[test]
1220 fn indentation_mark() {
1221 let mark = AdfMark::indentation(2);
1222 assert_eq!(mark.mark_type, "indentation");
1223 assert_eq!(mark.attrs.as_ref().unwrap()["level"], 2);
1224 }
1225
1226 #[test]
1227 fn breakout_mark() {
1228 let mark = AdfMark::breakout("wide");
1229 assert_eq!(mark.mark_type, "breakout");
1230 assert_eq!(mark.attrs.as_ref().unwrap()["mode"], "wide");
1231 }
1232}