1use crate::model::{
15 sort_keys_owned, sorted_value, Container, Invariant, Island, Line, LineKind, Loss, Mark,
16 MarkKind, Content,
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
134fn arr<'a>(obj: &'a Map<String, Value>, key: &'static str) -> Result<&'a Vec<Value>, ParseError> {
135 obj.get(key)
136 .and_then(Value::as_array)
137 .ok_or(ParseError::Shape(key))
138}
139
140pub fn line_kind_to_value(kind: &LineKind) -> Value {
147 let mut m = Map::new();
148 match kind {
149 LineKind::Para => {
150 m.insert("kind".into(), "para".into());
151 }
152 LineKind::Heading { level } => {
153 m.insert("kind".into(), "heading".into());
154 m.insert("level".into(), Value::from(*level));
155 }
156 LineKind::Code { lang } => {
157 m.insert("kind".into(), "code".into());
158 if let Some(l) = lang {
159 m.insert("lang".into(), Value::String(l.clone()));
160 }
161 }
162 LineKind::Island => {
163 m.insert("kind".into(), "island".into());
164 }
165 LineKind::Rule => {
166 m.insert("kind".into(), "rule".into());
167 }
168 }
169 Value::Object(m)
170}
171
172pub fn line_kind_from_value(v: &Value) -> Result<LineKind, ParseError> {
176 let o = v.as_object().ok_or(ParseError::Shape("line"))?;
177 match o.get("kind").and_then(Value::as_str) {
178 Some("para") => Ok(LineKind::Para),
179 Some("heading") => {
180 let level = o
181 .get("level")
182 .and_then(Value::as_u64)
183 .ok_or(ParseError::Shape("heading level"))?;
184 if !(1..=6).contains(&level) {
185 return Err(ParseError::Shape("heading level"));
186 }
187 Ok(LineKind::Heading { level: level as u8 })
188 }
189 Some("code") => Ok(LineKind::Code {
190 lang: o.get("lang").and_then(Value::as_str).map(str::to_string),
191 }),
192 Some("island") => Ok(LineKind::Island),
193 Some("rule") => Ok(LineKind::Rule),
194 _ => Err(ParseError::Shape("line kind")),
195 }
196}
197
198fn line_to_value(line: &Line) -> Value {
199 let Value::Object(mut m) = line_kind_to_value(&line.kind) else {
200 unreachable!("line_kind_to_value always returns an object")
201 };
202 m.insert(
203 "containers".into(),
204 Value::Array(line.containers.iter().map(container_to_value).collect()),
205 );
206 if line.continues {
209 m.insert("continues".into(), Value::Bool(true));
210 }
211 Value::Object(m)
212}
213
214fn line_from_value(v: &Value) -> Result<Line, ParseError> {
215 let o = v.as_object().ok_or(ParseError::Shape("line"))?;
216 let kind = line_kind_from_value(v)?;
217 let containers = o
218 .get("containers")
219 .and_then(Value::as_array)
220 .ok_or(ParseError::Shape("containers"))?
221 .iter()
222 .map(container_from_value)
223 .collect::<Result<_, _>>()?;
224 let continues = o.get("continues").and_then(Value::as_bool).unwrap_or(false);
225 Ok(Line {
226 kind,
227 containers,
228 continues,
229 })
230}
231
232pub fn container_to_value(c: &Container) -> Value {
236 let mut m = Map::new();
237 match c {
238 Container::ListItem {
239 ordered,
240 start,
241 ordinal,
242 } => {
243 m.insert("container".into(), "list_item".into());
244 m.insert("ordered".into(), Value::Bool(*ordered));
245 m.insert("start".into(), Value::from(*start));
246 m.insert("ordinal".into(), Value::from(*ordinal));
247 }
248 Container::Quote => {
249 m.insert("container".into(), "quote".into());
250 }
251 }
252 Value::Object(m)
253}
254
255pub fn container_from_value(v: &Value) -> Result<Container, ParseError> {
258 let o = v.as_object().ok_or(ParseError::Shape("container"))?;
259 match o.get("container").and_then(Value::as_str) {
260 Some("list_item") => Ok(Container::ListItem {
261 ordered: o.get("ordered").and_then(Value::as_bool).unwrap_or(false),
262 start: o.get("start").and_then(Value::as_u64).unwrap_or(1),
263 ordinal: o.get("ordinal").and_then(Value::as_u64).unwrap_or(0),
264 }),
265 Some("quote") => Ok(Container::Quote),
266 _ => Err(ParseError::Shape("container kind")),
267 }
268}
269
270pub fn mark_to_value(mark: &Mark) -> Value {
276 let mut m = Map::new();
277 m.insert("start".into(), Value::from(mark.start));
278 m.insert("end".into(), Value::from(mark.end));
279 match &mark.kind {
280 MarkKind::Strong => {
281 m.insert("type".into(), "strong".into());
282 }
283 MarkKind::Emph => {
284 m.insert("type".into(), "emph".into());
285 }
286 MarkKind::Underline => {
287 m.insert("type".into(), "underline".into());
288 }
289 MarkKind::Strike => {
290 m.insert("type".into(), "strike".into());
291 }
292 MarkKind::Code => {
293 m.insert("type".into(), "code".into());
294 }
295 MarkKind::Link { url } => {
296 m.insert("type".into(), "link".into());
297 m.insert("url".into(), Value::String(url.clone()));
298 }
299 MarkKind::Anchor { id } => {
300 m.insert("type".into(), "anchor".into());
301 m.insert("id".into(), Value::String(id.clone()));
302 }
303 MarkKind::Unknown { tag, attrs } => {
304 m.insert("type".into(), Value::String(tag.clone()));
305 m.insert("attrs".into(), sorted_value(attrs));
306 }
307 }
308 Value::Object(m)
309}
310
311pub fn mark_from_value(v: &Value) -> Result<Mark, ParseError> {
315 let o = v.as_object().ok_or(ParseError::Shape("mark"))?;
316 let start = o
317 .get("start")
318 .and_then(Value::as_u64)
319 .ok_or(ParseError::Shape("mark start"))? as usize;
320 let end = o
321 .get("end")
322 .and_then(Value::as_u64)
323 .ok_or(ParseError::Shape("mark end"))? as usize;
324 let ty = o
325 .get("type")
326 .and_then(Value::as_str)
327 .ok_or(ParseError::Shape("mark type"))?;
328 let kind = match ty {
329 "strong" => MarkKind::Strong,
330 "emph" => MarkKind::Emph,
331 "underline" => MarkKind::Underline,
332 "strike" => MarkKind::Strike,
333 "code" => MarkKind::Code,
334 "link" => MarkKind::Link {
335 url: o
336 .get("url")
337 .and_then(Value::as_str)
338 .unwrap_or_default()
339 .to_string(),
340 },
341 "anchor" => MarkKind::Anchor {
342 id: o
343 .get("id")
344 .and_then(Value::as_str)
345 .unwrap_or_default()
346 .to_string(),
347 },
348 other => MarkKind::Unknown {
351 tag: other.to_string(),
352 attrs: o.get("attrs").cloned().unwrap_or(Value::Null),
353 },
354 };
355 Ok(Mark { start, end, kind })
356}
357
358pub fn parse_cell(v: &Value) -> (String, Vec<Mark>) {
372 let text = v
373 .get("text")
374 .and_then(Value::as_str)
375 .unwrap_or_default()
376 .to_string();
377 let marks = v
378 .get("marks")
379 .and_then(Value::as_array)
380 .map(|arr| arr.iter().filter_map(|m| mark_from_value(m).ok()).collect())
381 .unwrap_or_default();
382 (text, marks)
383}
384
385pub(crate) fn cell_to_value(text: &str, marks: &[Mark]) -> Value {
389 let mut m = Map::new();
390 m.insert("text".into(), Value::String(text.to_string()));
391 m.insert(
392 "marks".into(),
393 Value::Array(marks.iter().map(mark_to_value).collect()),
394 );
395 Value::Object(m)
396}
397
398pub(crate) fn table_cells(props: &Value) -> Vec<(String, Vec<Mark>)> {
401 let mut out = Vec::new();
402 if let Some(h) = props.get("header").and_then(Value::as_array) {
403 out.extend(h.iter().map(parse_cell));
404 }
405 if let Some(rows) = props.get("rows").and_then(Value::as_array) {
406 for row in rows {
407 if let Some(r) = row.as_array() {
408 out.extend(r.iter().map(parse_cell));
409 }
410 }
411 }
412 out
413}
414
415fn island_is_mark_carrying(island_type: &str) -> bool {
422 matches!(island_type, "table")
423}
424
425pub(crate) fn normalize_island_structure(island: &mut Island) {
429 if island_is_mark_carrying(&island.island_type) {
430 normalize_table_props(&mut island.props);
431 }
432}
433
434pub(crate) fn island_cell_marks(island: &Island) -> Vec<(String, Vec<Mark>)> {
438 if island_is_mark_carrying(&island.island_type) {
439 table_cells(&island.props)
440 } else {
441 Vec::new()
442 }
443}
444
445pub(crate) fn island_shape_error(island: &Island) -> Option<Invariant> {
449 if island_is_mark_carrying(&island.island_type) {
450 table_shape_error(&island.props)
451 } else {
452 None
453 }
454}
455
456fn normalize_table_props(props: &mut Value) {
469 let cols = table_cols(props);
470 let Some(obj) = props.as_object_mut() else {
471 return;
472 };
473 let header = obj.entry("header").or_insert_with(|| Value::Array(vec![]));
474 if !header.is_array() {
478 *header = Value::Array(vec![]);
479 }
480 pad_row(header, cols);
481 if let Some(h) = header.as_array_mut() {
482 h.iter_mut().for_each(canon_cell);
483 }
484 let aligns = obj.entry("aligns").or_insert_with(|| Value::Array(vec![]));
485 if let Some(a) = aligns.as_array_mut() {
486 while a.len() < cols {
487 a.push(Value::String("none".into()));
488 }
489 }
490 if let Some(rows) = obj.get_mut("rows").and_then(Value::as_array_mut) {
491 for row in rows.iter_mut() {
492 pad_row(row, cols);
493 if let Some(r) = row.as_array_mut() {
494 r.iter_mut().for_each(canon_cell);
495 }
496 }
497 }
498}
499
500fn table_cols(props: &Value) -> usize {
503 let arr_len = |k: &str| props.get(k).and_then(Value::as_array).map(|a| a.len());
504 let header = arr_len("header").unwrap_or(0);
505 let aligns = arr_len("aligns").unwrap_or(0);
506 let widest_row = props
507 .get("rows")
508 .and_then(Value::as_array)
509 .map(|rows| {
510 rows.iter()
511 .map(|r| r.as_array().map(|a| a.len()).unwrap_or(0))
512 .max()
513 .unwrap_or(0)
514 })
515 .unwrap_or(0);
516 header.max(aligns).max(widest_row)
517}
518
519fn pad_row(v: &mut Value, cols: usize) {
522 if let Some(arr) = v.as_array_mut() {
523 while arr.len() < cols {
524 arr.push(cell_to_value("", &[]));
525 }
526 }
527}
528
529fn canon_cell(cell: &mut Value) {
532 let (text, marks) = parse_cell(cell);
533 let text = if text.contains(['\n', '\r']) {
534 text.replace(['\n', '\r'], " ")
535 } else {
536 text
537 };
538 *cell = cell_to_value(&text, &crate::model::normalize_marks(marks));
539}
540
541fn table_shape_error(props: &Value) -> Option<Invariant> {
545 if props.get("header").is_some_and(|h| !h.is_array()) {
550 return Some(Invariant::TableHeaderNotArray);
551 }
552 let cols = props
553 .get("header")
554 .and_then(Value::as_array)
555 .map(|a| a.len())
556 .unwrap_or(0);
557 let aligns = props
558 .get("aligns")
559 .and_then(Value::as_array)
560 .map(|a| a.len())
561 .unwrap_or(0);
562 if aligns != cols {
563 return Some(Invariant::TableAlignsMismatch { aligns, cols });
564 }
565 if let Some(rows) = props.get("rows").and_then(Value::as_array) {
566 for (i, row) in rows.iter().enumerate() {
567 let width = row.as_array().map(|a| a.len()).unwrap_or(0);
568 if width != cols {
569 return Some(Invariant::TableRaggedRow {
570 row: i,
571 width,
572 cols,
573 });
574 }
575 }
576 }
577 for (i, (text, _)) in table_cells(props).iter().enumerate() {
578 if text.contains('\n') || text.contains('\r') {
579 return Some(Invariant::TableCellNewline { cell: i });
580 }
581 }
582 None
583}
584
585fn island_to_value(island: &Island) -> Value {
588 let mut m = Map::new();
589 m.insert("id".into(), Value::String(island.id.clone()));
590 m.insert("type".into(), Value::String(island.island_type.clone()));
591 m.insert("props".into(), sorted_value(&island.props));
592 m.insert("loss".into(), loss_to_str(island.loss).into());
593 Value::Object(m)
594}
595
596fn island_from_value(v: &Value) -> Result<Island, ParseError> {
597 let o = v.as_object().ok_or(ParseError::Shape("island"))?;
598 Ok(Island {
599 id: o
600 .get("id")
601 .and_then(Value::as_str)
602 .ok_or(ParseError::Shape("island id"))?
603 .to_string(),
604 island_type: o
605 .get("type")
606 .and_then(Value::as_str)
607 .ok_or(ParseError::Shape("island type"))?
608 .to_string(),
609 props: o.get("props").cloned().unwrap_or(Value::Null),
610 loss: loss_from_str(o.get("loss").and_then(Value::as_str).unwrap_or("lossless")),
611 })
612}
613
614fn loss_to_str(loss: Loss) -> &'static str {
615 match loss {
616 Loss::Lossless => "lossless",
617 Loss::Degraded => "degraded",
618 Loss::Unrepresentable => "unrepresentable",
619 }
620}
621
622fn loss_from_str(s: &str) -> Loss {
623 match s {
624 "lossless" => Loss::Lossless,
625 "degraded" => Loss::Degraded,
626 _ => Loss::Unrepresentable,
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635 use crate::model::{Line, LineKind};
636
637 fn sample() -> Content {
638 Content {
639 text: "hello world".into(),
640 lines: vec![Line {
641 kind: LineKind::Para,
642 containers: vec![],
643 continues: false,
644 }],
645 marks: vec![
646 Mark {
647 start: 6,
648 end: 11,
649 kind: MarkKind::Strong,
650 },
651 Mark {
652 start: 0,
653 end: 5,
654 kind: MarkKind::Emph,
655 },
656 ],
657 islands: vec![],
658 }
659 }
660
661 #[test]
662 fn island_props_key_order_does_not_leak() {
663 let mut one = Content::empty();
664 one.text = "\u{FFFC}".into();
665 one.lines = vec![Line {
666 kind: LineKind::Island,
667 containers: vec![],
668 continues: false,
669 }];
670 one.islands = vec![Island {
671 id: "i1".into(),
672 island_type: "table".into(),
673 props: serde_json::json!({"b": 1, "a": 2}),
674 loss: Loss::Lossless,
675 }];
676 let mut two = one.clone();
677 two.islands[0].props = serde_json::json!({"a": 2, "b": 1}); assert_eq!(one.to_canonical_json(), two.to_canonical_json());
679 }
680
681 #[test]
682 fn golden_bytes_are_feature_independent() {
683 let rt = sample();
687 assert_eq!(
688 rt.to_canonical_json(),
689 r#"{"islands":[],"lines":[{"containers":[],"kind":"para"}],"marks":[{"end":5,"start":0,"type":"emph"},{"end":11,"start":6,"type":"strong"}],"text":"hello world"}"#
690 );
691 }
692
693 #[test]
694 fn from_canonical_json_rejects_invalid() {
695 let bad =
697 r#"{"text":"a\nb","lines":[{"kind":"para","containers":[]}],"marks":[],"islands":[]}"#;
698 assert!(matches!(
699 Content::from_canonical_json(bad),
700 Err(ParseError::Invalid(_))
701 ));
702 }
703
704 #[test]
705 fn reserved_unknown_tag_rejected() {
706 let mut rt = Content::empty();
709 rt.text = "abcd".into();
710 rt.marks = vec![Mark {
711 start: 0,
712 end: 4,
713 kind: MarkKind::Unknown {
714 tag: "strong".into(),
715 attrs: serde_json::json!({}),
716 },
717 }];
718 assert!(matches!(
719 rt.validate(),
720 Err(crate::model::Invariant::ReservedUnknownTag(_))
721 ));
722 }
723
724 #[test]
725 fn unknown_loss_class_defaults_unrepresentable() {
726 let json = r#"{"text":"","lines":[{"kind":"island","containers":[]}],"marks":[],"islands":[{"id":"i1","type":"widget","props":{},"loss":"future_class"}]}"#;
727 let rt = Content::from_canonical_json(json).unwrap();
728 assert_eq!(rt.islands[0].loss, Loss::Unrepresentable);
729 }
730
731 #[test]
732 fn unknown_mark_round_trips_opaque() {
733 let mut rt = Content::empty();
734 rt.text = "abcd".into();
735 rt.marks = vec![Mark {
736 start: 0,
737 end: 4,
738 kind: MarkKind::Unknown {
739 tag: "highlight".into(),
740 attrs: serde_json::json!({"color": "yellow"}),
741 },
742 }];
743 let json = rt.to_canonical_json();
744 let back = Content::from_canonical_json(&json).unwrap();
745 assert_eq!(back.marks[0].kind, rt.marks[0].kind);
746 }
747}