1use crate::model::{
15 sort_keys_owned, sorted_value, Container, Invariant, Island, Line, LineKind, Loss, Mark,
16 MarkKind, Content, Usv,
17};
18use serde_json::{Map, Value};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum ParseError {
24 Shape(&'static str),
26 Json(String),
28 Invalid(crate::model::Invariant),
30}
31
32impl std::fmt::Display for ParseError {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 ParseError::Shape(s) => write!(f, "content json shape: {s}"),
36 ParseError::Json(s) => write!(f, "content json parse: {s}"),
37 ParseError::Invalid(inv) => write!(f, "content invariant: {inv:?}"),
38 }
39 }
40}
41impl std::error::Error for ParseError {}
42
43impl Content {
44 pub fn to_canonical_json(&self) -> String {
50 to_canonical_value(self).to_string()
51 }
52
53 pub fn from_canonical_json(s: &str) -> Result<Content, ParseError> {
59 let v: Value = serde_json::from_str(s).map_err(|e| ParseError::Json(e.to_string()))?;
60 from_canonical_value(&v)
61 }
62
63 fn to_value(&self) -> Value {
64 let mut root = Map::new();
65 root.insert("text".into(), Value::String(self.text.clone()));
66 root.insert(
67 "lines".into(),
68 Value::Array(self.lines.iter().map(line_to_value).collect()),
69 );
70 root.insert(
71 "marks".into(),
72 Value::Array(self.marks.iter().map(mark_to_value).collect()),
73 );
74 root.insert(
75 "islands".into(),
76 Value::Array(self.islands.iter().map(island_to_value).collect()),
77 );
78 Value::Object(root)
79 }
80
81 fn from_value(v: &Value) -> Result<Content, ParseError> {
82 let obj = v.as_object().ok_or(ParseError::Shape("root not object"))?;
83 let text = obj
84 .get("text")
85 .and_then(Value::as_str)
86 .ok_or(ParseError::Shape("text"))?
87 .to_string();
88 let lines = arr(obj, "lines")?
89 .iter()
90 .map(line_from_value)
91 .collect::<Result<_, _>>()?;
92 let marks = arr(obj, "marks")?
93 .iter()
94 .map(mark_from_value)
95 .collect::<Result<_, _>>()?;
96 let islands = arr(obj, "islands")?
97 .iter()
98 .map(island_from_value)
99 .collect::<Result<_, _>>()?;
100 Ok(Content {
101 text,
102 lines,
103 marks,
104 islands,
105 })
106 }
107}
108
109pub fn to_canonical_value(rt: &Content) -> Value {
117 let mut rt = rt.clone();
118 rt.normalize();
119 sort_keys_owned(rt.to_value())
120}
121
122pub fn from_canonical_value(v: &Value) -> Result<Content, ParseError> {
128 let mut rt = Content::from_value(v)?;
129 rt.normalize();
130 rt.validate().map_err(ParseError::Invalid)?;
131 Ok(rt)
132}
133
134pub(crate) fn usv_from(v: Option<&Value>, what: &'static str) -> Result<Usv, ParseError> {
139 let n = v.and_then(Value::as_u64).ok_or(ParseError::Shape(what))?;
140 Usv::try_from(n).map_err(|_| ParseError::Shape(what))
141}
142
143fn arr<'a>(obj: &'a Map<String, Value>, key: &'static str) -> Result<&'a Vec<Value>, ParseError> {
144 obj.get(key)
145 .and_then(Value::as_array)
146 .ok_or(ParseError::Shape(key))
147}
148
149pub fn line_kind_to_value(kind: &LineKind) -> Value {
156 let mut m = Map::new();
157 match kind {
158 LineKind::Para => {
159 m.insert("kind".into(), "para".into());
160 }
161 LineKind::Heading { level } => {
162 m.insert("kind".into(), "heading".into());
163 m.insert("level".into(), Value::from(*level));
164 }
165 LineKind::Code { lang } => {
166 m.insert("kind".into(), "code".into());
167 if let Some(l) = lang {
168 m.insert("lang".into(), Value::String(l.clone()));
169 }
170 }
171 LineKind::Island => {
172 m.insert("kind".into(), "island".into());
173 }
174 LineKind::Rule => {
175 m.insert("kind".into(), "rule".into());
176 }
177 LineKind::Unknown { tag, attrs } => {
181 m.insert("kind".into(), Value::String(tag.clone()));
182 m.insert("attrs".into(), sorted_value(attrs));
183 }
184 }
185 Value::Object(m)
186}
187
188pub fn line_kind_from_value(v: &Value) -> Result<LineKind, ParseError> {
192 let o = v.as_object().ok_or(ParseError::Shape("line"))?;
193 match o.get("kind").and_then(Value::as_str) {
194 Some("para") => Ok(LineKind::Para),
195 Some("heading") => {
196 let level = o
197 .get("level")
198 .and_then(Value::as_u64)
199 .ok_or(ParseError::Shape("heading level"))?;
200 if !(1..=6).contains(&level) {
201 return Err(ParseError::Shape("heading level"));
202 }
203 Ok(LineKind::Heading { level: level as u8 })
204 }
205 Some("code") => Ok(LineKind::Code {
206 lang: o.get("lang").and_then(Value::as_str).map(str::to_string),
207 }),
208 Some("island") => Ok(LineKind::Island),
209 Some("rule") => Ok(LineKind::Rule),
210 Some(other) => Ok(LineKind::Unknown {
214 tag: other.to_string(),
215 attrs: o.get("attrs").cloned().unwrap_or(Value::Null),
216 }),
217 None => Err(ParseError::Shape("line kind")),
218 }
219}
220
221fn line_to_value(line: &Line) -> Value {
222 let Value::Object(mut m) = line_kind_to_value(&line.kind) else {
223 unreachable!("line_kind_to_value always returns an object")
224 };
225 m.insert(
226 "containers".into(),
227 Value::Array(line.containers.iter().map(container_to_value).collect()),
228 );
229 if line.continues {
232 m.insert("continues".into(), Value::Bool(true));
233 }
234 Value::Object(m)
235}
236
237fn line_from_value(v: &Value) -> Result<Line, ParseError> {
238 let o = v.as_object().ok_or(ParseError::Shape("line"))?;
239 let kind = line_kind_from_value(v)?;
240 let containers = o
241 .get("containers")
242 .and_then(Value::as_array)
243 .ok_or(ParseError::Shape("containers"))?
244 .iter()
245 .map(container_from_value)
246 .collect::<Result<_, _>>()?;
247 let continues = o.get("continues").and_then(Value::as_bool).unwrap_or(false);
248 Ok(Line {
249 kind,
250 containers,
251 continues,
252 })
253}
254
255pub fn container_to_value(c: &Container) -> Value {
259 let mut m = Map::new();
260 match c {
261 Container::ListItem {
262 ordered,
263 start,
264 ordinal,
265 } => {
266 m.insert("container".into(), "list_item".into());
267 m.insert("ordered".into(), Value::Bool(*ordered));
268 m.insert("start".into(), Value::from(*start));
269 m.insert("ordinal".into(), Value::from(*ordinal));
270 }
271 Container::Quote => {
272 m.insert("container".into(), "quote".into());
273 }
274 Container::Unknown { tag, attrs } => {
275 m.insert("container".into(), Value::String(tag.clone()));
276 m.insert("attrs".into(), sorted_value(attrs));
277 }
278 }
279 Value::Object(m)
280}
281
282pub fn container_from_value(v: &Value) -> Result<Container, ParseError> {
285 let o = v.as_object().ok_or(ParseError::Shape("container"))?;
286 match o.get("container").and_then(Value::as_str) {
287 Some("list_item") => Ok(Container::ListItem {
288 ordered: o.get("ordered").and_then(Value::as_bool).unwrap_or(false),
289 start: o.get("start").and_then(Value::as_u64).unwrap_or(1),
290 ordinal: o.get("ordinal").and_then(Value::as_u64).unwrap_or(0),
291 }),
292 Some("quote") => Ok(Container::Quote),
293 Some(other) => Ok(Container::Unknown {
296 tag: other.to_string(),
297 attrs: o.get("attrs").cloned().unwrap_or(Value::Null),
298 }),
299 None => Err(ParseError::Shape("container kind")),
300 }
301}
302
303pub fn mark_to_value(mark: &Mark) -> Value {
309 let mut m = Map::new();
310 m.insert("start".into(), Value::from(mark.start));
311 m.insert("end".into(), Value::from(mark.end));
312 match &mark.kind {
313 MarkKind::Strong => {
314 m.insert("type".into(), "strong".into());
315 }
316 MarkKind::Emph => {
317 m.insert("type".into(), "emph".into());
318 }
319 MarkKind::Underline => {
320 m.insert("type".into(), "underline".into());
321 }
322 MarkKind::Strike => {
323 m.insert("type".into(), "strike".into());
324 }
325 MarkKind::Code => {
326 m.insert("type".into(), "code".into());
327 }
328 MarkKind::Link { url } => {
329 m.insert("type".into(), "link".into());
330 m.insert("url".into(), Value::String(url.clone()));
331 }
332 MarkKind::Anchor { id } => {
333 m.insert("type".into(), "anchor".into());
334 m.insert("id".into(), Value::String(id.clone()));
335 }
336 MarkKind::Unknown { tag, attrs } => {
337 m.insert("type".into(), Value::String(tag.clone()));
338 m.insert("attrs".into(), sorted_value(attrs));
339 }
340 }
341 Value::Object(m)
342}
343
344pub fn mark_from_value(v: &Value) -> Result<Mark, ParseError> {
348 let o = v.as_object().ok_or(ParseError::Shape("mark"))?;
349 let start = usv_from(o.get("start"), "mark start")?;
350 let end = usv_from(o.get("end"), "mark end")?;
351 let ty = o
352 .get("type")
353 .and_then(Value::as_str)
354 .ok_or(ParseError::Shape("mark type"))?;
355 let kind = match ty {
356 "strong" => MarkKind::Strong,
357 "emph" => MarkKind::Emph,
358 "underline" => MarkKind::Underline,
359 "strike" => MarkKind::Strike,
360 "code" => MarkKind::Code,
361 "link" => MarkKind::Link {
362 url: o
363 .get("url")
364 .and_then(Value::as_str)
365 .unwrap_or_default()
366 .to_string(),
367 },
368 "anchor" => MarkKind::Anchor {
369 id: o
370 .get("id")
371 .and_then(Value::as_str)
372 .unwrap_or_default()
373 .to_string(),
374 },
375 other => MarkKind::Unknown {
378 tag: other.to_string(),
379 attrs: o.get("attrs").cloned().unwrap_or(Value::Null),
380 },
381 };
382 Ok(Mark { start, end, kind })
383}
384
385pub fn parse_cell(v: &Value) -> (String, Vec<Mark>) {
399 let text = v
400 .get("text")
401 .and_then(Value::as_str)
402 .unwrap_or_default()
403 .to_string();
404 let marks = v
405 .get("marks")
406 .and_then(Value::as_array)
407 .map(|arr| arr.iter().filter_map(|m| mark_from_value(m).ok()).collect())
408 .unwrap_or_default();
409 (text, marks)
410}
411
412pub(crate) fn cell_to_value(text: &str, marks: &[Mark]) -> Value {
416 let mut m = Map::new();
417 m.insert("text".into(), Value::String(text.to_string()));
418 m.insert(
419 "marks".into(),
420 Value::Array(marks.iter().map(mark_to_value).collect()),
421 );
422 Value::Object(m)
423}
424
425pub(crate) fn table_cells(props: &Value) -> Vec<(String, Vec<Mark>)> {
428 let mut out = Vec::new();
429 if let Some(h) = props.get("header").and_then(Value::as_array) {
430 out.extend(h.iter().map(parse_cell));
431 }
432 if let Some(rows) = props.get("rows").and_then(Value::as_array) {
433 for row in rows {
434 if let Some(r) = row.as_array() {
435 out.extend(r.iter().map(parse_cell));
436 }
437 }
438 }
439 out
440}
441
442pub(crate) fn normalize_table_props(props: &mut Value) {
459 let cols = table_cols(props);
460 let Some(obj) = props.as_object_mut() else {
461 return;
462 };
463 let header = obj.entry("header").or_insert_with(|| Value::Array(vec![]));
464 if !header.is_array() {
468 *header = Value::Array(vec![]);
469 }
470 pad_row(header, cols);
471 if let Some(h) = header.as_array_mut() {
472 h.iter_mut().for_each(canon_cell);
473 }
474 let aligns = obj.entry("aligns").or_insert_with(|| Value::Array(vec![]));
475 if let Some(a) = aligns.as_array_mut() {
476 while a.len() < cols {
477 a.push(Value::String("none".into()));
478 }
479 }
480 if let Some(rows) = obj.get_mut("rows").and_then(Value::as_array_mut) {
481 for row in rows.iter_mut() {
482 pad_row(row, cols);
483 if let Some(r) = row.as_array_mut() {
484 r.iter_mut().for_each(canon_cell);
485 }
486 }
487 }
488}
489
490fn table_cols(props: &Value) -> usize {
493 let arr_len = |k: &str| props.get(k).and_then(Value::as_array).map(|a| a.len());
494 let header = arr_len("header").unwrap_or(0);
495 let aligns = arr_len("aligns").unwrap_or(0);
496 let widest_row = props
497 .get("rows")
498 .and_then(Value::as_array)
499 .map(|rows| {
500 rows.iter()
501 .map(|r| r.as_array().map(|a| a.len()).unwrap_or(0))
502 .max()
503 .unwrap_or(0)
504 })
505 .unwrap_or(0);
506 header.max(aligns).max(widest_row)
507}
508
509fn pad_row(v: &mut Value, cols: usize) {
512 if let Some(arr) = v.as_array_mut() {
513 while arr.len() < cols {
514 arr.push(cell_to_value("", &[]));
515 }
516 }
517}
518
519fn canon_cell(cell: &mut Value) {
522 let (text, marks) = parse_cell(cell);
523 let text = if text.contains(['\n', '\r']) {
524 text.replace(['\n', '\r'], " ")
525 } else {
526 text
527 };
528 *cell = cell_to_value(&text, &crate::model::normalize_marks(marks));
529}
530
531pub(crate) fn table_shape_error(props: &Value) -> Option<Invariant> {
535 if props.get("header").is_some_and(|h| !h.is_array()) {
540 return Some(Invariant::TableHeaderNotArray);
541 }
542 let cols = props
543 .get("header")
544 .and_then(Value::as_array)
545 .map(|a| a.len())
546 .unwrap_or(0);
547 let aligns = props
548 .get("aligns")
549 .and_then(Value::as_array)
550 .map(|a| a.len())
551 .unwrap_or(0);
552 if aligns != cols {
553 return Some(Invariant::TableAlignsMismatch { aligns, cols });
554 }
555 if let Some(rows) = props.get("rows").and_then(Value::as_array) {
556 for (i, row) in rows.iter().enumerate() {
557 let width = row.as_array().map(|a| a.len()).unwrap_or(0);
558 if width != cols {
559 return Some(Invariant::TableRaggedRow {
560 row: i,
561 width,
562 cols,
563 });
564 }
565 }
566 }
567 for (i, (text, _)) in table_cells(props).iter().enumerate() {
568 if text.contains('\n') || text.contains('\r') {
569 return Some(Invariant::TableCellNewline { cell: i });
570 }
571 }
572 None
573}
574
575fn island_to_value(island: &Island) -> Value {
578 let mut m = Map::new();
579 m.insert("id".into(), Value::String(island.id.clone()));
580 m.insert("type".into(), Value::String(island.island_type.clone()));
581 m.insert("props".into(), sorted_value(&island.props));
582 m.insert("loss".into(), loss_to_str(island.loss).into());
583 Value::Object(m)
584}
585
586fn island_from_value(v: &Value) -> Result<Island, ParseError> {
587 let o = v.as_object().ok_or(ParseError::Shape("island"))?;
588 Ok(Island {
589 id: o
590 .get("id")
591 .and_then(Value::as_str)
592 .ok_or(ParseError::Shape("island id"))?
593 .to_string(),
594 island_type: o
595 .get("type")
596 .and_then(Value::as_str)
597 .ok_or(ParseError::Shape("island type"))?
598 .to_string(),
599 props: o.get("props").cloned().unwrap_or(Value::Null),
600 loss: loss_from_str(o.get("loss").and_then(Value::as_str).unwrap_or("lossless")),
601 })
602}
603
604fn loss_to_str(loss: Loss) -> &'static str {
605 match loss {
606 Loss::Lossless => "lossless",
607 Loss::Degraded => "degraded",
608 Loss::Unrepresentable => "unrepresentable",
609 }
610}
611
612fn loss_from_str(s: &str) -> Loss {
613 match s {
614 "lossless" => Loss::Lossless,
615 "degraded" => Loss::Degraded,
616 _ => Loss::Unrepresentable,
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625 use crate::model::{Line, LineKind};
626
627 fn sample() -> Content {
628 Content {
629 text: "hello world".into(),
630 lines: vec![Line {
631 kind: LineKind::Para,
632 containers: vec![],
633 continues: false,
634 }],
635 marks: vec![
636 Mark {
637 start: 6,
638 end: 11,
639 kind: MarkKind::Strong,
640 },
641 Mark {
642 start: 0,
643 end: 5,
644 kind: MarkKind::Emph,
645 },
646 ],
647 islands: vec![],
648 }
649 }
650
651 #[test]
656 fn deep_container_nesting_is_rejected_at_decode() {
657 let containers = vec![r#"{"container":"quote"}"#; 20_000].join(",");
658 let json = format!(
659 r#"{{"text":"hi","lines":[{{"kind":"para","containers":[{containers}]}}],"marks":[],"islands":[]}}"#
660 );
661 assert!(matches!(
662 Content::from_canonical_json(&json),
663 Err(ParseError::Invalid(Invariant::NestingTooDeep { .. }))
664 ));
665 }
666
667 #[test]
673 fn out_of_range_wire_position_is_refused() {
674 let json = r#"{"text":"hello","lines":[{"kind":"para","containers":[]}],"marks":[{"start":4294967301,"end":4294967302,"type":"strong"}],"islands":[]}"#;
675 assert!(Content::from_canonical_json(json).is_err());
676 assert!(usv_from(Some(&Value::from(u64::MAX)), "x").is_ok() || usize::BITS < 64);
677 assert!(usv_from(Some(&Value::from(-1i64)), "x").is_err());
678 }
679
680 #[test]
681 fn island_props_key_order_does_not_leak() {
682 let mut one = Content::empty();
683 one.text = "\u{FFFC}".into();
684 one.lines = vec![Line {
685 kind: LineKind::Island,
686 containers: vec![],
687 continues: false,
688 }];
689 one.islands = vec![Island {
690 id: "i1".into(),
691 island_type: "table".into(),
692 props: serde_json::json!({"b": 1, "a": 2}),
693 loss: Loss::Lossless,
694 }];
695 let mut two = one.clone();
696 two.islands[0].props = serde_json::json!({"a": 2, "b": 1}); assert_eq!(one.to_canonical_json(), two.to_canonical_json());
698 }
699
700 #[test]
701 fn golden_bytes_are_feature_independent() {
702 let rt = sample();
706 assert_eq!(
707 rt.to_canonical_json(),
708 r#"{"islands":[],"lines":[{"containers":[],"kind":"para"}],"marks":[{"end":5,"start":0,"type":"emph"},{"end":11,"start":6,"type":"strong"}],"text":"hello world"}"#
709 );
710 }
711
712 #[test]
713 fn from_canonical_json_rejects_invalid() {
714 let bad =
716 r#"{"text":"a\nb","lines":[{"kind":"para","containers":[]}],"marks":[],"islands":[]}"#;
717 assert!(matches!(
718 Content::from_canonical_json(bad),
719 Err(ParseError::Invalid(_))
720 ));
721 }
722
723 #[test]
724 fn reserved_unknown_tag_rejected() {
725 let mut rt = Content::empty();
728 rt.text = "abcd".into();
729 rt.marks = vec![Mark {
730 start: 0,
731 end: 4,
732 kind: MarkKind::Unknown {
733 tag: "strong".into(),
734 attrs: serde_json::json!({}),
735 },
736 }];
737 assert!(matches!(
738 rt.validate(),
739 Err(crate::model::Invariant::ReservedUnknownTag(_))
740 ));
741 }
742
743 #[test]
744 fn unknown_loss_class_defaults_unrepresentable() {
745 let json = r#"{"text":"","lines":[{"kind":"island","containers":[]}],"marks":[],"islands":[{"id":"i1","type":"widget","props":{},"loss":"future_class"}]}"#;
746 let rt = Content::from_canonical_json(json).unwrap();
747 assert_eq!(rt.islands[0].loss, Loss::Unrepresentable);
748 }
749
750 #[test]
755 fn unknown_line_kind_and_container_round_trip_opaque() {
756 let json = concat!(
757 r#"{"islands":[],"lines":[{"attrs":{"variant":"warn"},"containers":"#,
758 r#"[{"attrs":{"depth":2},"container":"indent"}],"kind":"callout"}],"#,
759 r#""marks":[],"text":"heads up"}"#
760 );
761 let rt = Content::from_canonical_json(json).unwrap();
762 assert_eq!(
763 rt.lines[0].kind,
764 LineKind::Unknown {
765 tag: "callout".into(),
766 attrs: serde_json::json!({"variant": "warn"}),
767 }
768 );
769 assert_eq!(
770 rt.lines[0].containers,
771 vec![Container::Unknown {
772 tag: "indent".into(),
773 attrs: serde_json::json!({"depth": 2}),
774 }]
775 );
776 assert_eq!(rt.to_canonical_json(), json);
777 let bare = r#"{"islands":[],"lines":[{"containers":[],"kind":"footnote"}],"marks":[],"text":"x"}"#;
779 let rt = Content::from_canonical_json(bare).unwrap();
780 assert_eq!(
781 rt.lines[0].kind,
782 LineKind::Unknown {
783 tag: "footnote".into(),
784 attrs: Value::Null,
785 }
786 );
787 for bad in [
790 r#"{"islands":[],"lines":[{"containers":[]}],"marks":[],"text":"x"}"#,
791 r#"{"islands":[],"lines":[{"containers":[{"container":7}],"kind":"para"}],"marks":[],"text":"x"}"#,
792 ] {
793 assert!(matches!(
794 Content::from_canonical_json(bad),
795 Err(ParseError::Shape(_))
796 ));
797 }
798 }
799
800 #[test]
804 fn reserved_block_vocabulary_names_rejected() {
805 let mut rt = Content::empty();
806 rt.text = "abcd".into();
807 rt.lines[0].kind = LineKind::Unknown {
808 tag: "heading".into(),
809 attrs: serde_json::json!({}),
810 };
811 assert_eq!(
812 rt.validate(),
813 Err(Invariant::ReservedUnknownLineKind("heading".into()))
814 );
815 rt.lines[0].kind = LineKind::Para;
816 rt.lines[0].containers = vec![Container::Unknown {
817 tag: "quote".into(),
818 attrs: serde_json::json!({}),
819 }];
820 assert_eq!(
821 rt.validate(),
822 Err(Invariant::ReservedUnknownContainer("quote".into()))
823 );
824 }
825
826 #[test]
829 fn unknown_block_attrs_key_order_does_not_leak() {
830 let mut one = Content::empty();
831 one.text = "hi".into();
832 one.lines[0].kind = LineKind::Unknown {
833 tag: "callout".into(),
834 attrs: serde_json::json!({"b": 1, "a": 2}),
835 };
836 one.lines[0].containers = vec![Container::Unknown {
837 tag: "indent".into(),
838 attrs: serde_json::json!({"y": 1, "x": 2}),
839 }];
840 let mut two = one.clone();
841 two.lines[0].kind = LineKind::Unknown {
842 tag: "callout".into(),
843 attrs: serde_json::json!({"a": 2, "b": 1}),
844 };
845 two.lines[0].containers = vec![Container::Unknown {
846 tag: "indent".into(),
847 attrs: serde_json::json!({"x": 2, "y": 1}),
848 }];
849 assert_eq!(one.to_canonical_json(), two.to_canonical_json());
850 one.normalize();
851 two.normalize();
852 assert_eq!(one, two, "normalize canonicalizes the live model too");
853 }
854
855 #[test]
856 fn unknown_mark_round_trips_opaque() {
857 let mut rt = Content::empty();
858 rt.text = "abcd".into();
859 rt.marks = vec![Mark {
860 start: 0,
861 end: 4,
862 kind: MarkKind::Unknown {
863 tag: "highlight".into(),
864 attrs: serde_json::json!({"color": "yellow"}),
865 },
866 }];
867 let json = rt.to_canonical_json();
868 let back = Content::from_canonical_json(&json).unwrap();
869 assert_eq!(back.marks[0].kind, rt.marks[0].kind);
870 }
871}