1use fig::{Embed, EmbedType, Segment};
14
15use prov_graph::document::MetaCarrier;
16use prov_graph::meta::Mapping;
17use prov_graph::{Error, Result};
18
19fn default_embed_type() -> EmbedType {
24 #[cfg(feature = "yaml")]
25 return EmbedType::FrontmatterYaml;
26 #[cfg(all(not(feature = "yaml"), feature = "json"))]
27 return EmbedType::FrontmatterJson;
28 #[cfg(all(not(feature = "yaml"), not(feature = "json"), feature = "toml"))]
29 return EmbedType::PlusToml;
30 #[cfg(all(
31 not(feature = "yaml"),
32 not(feature = "json"),
33 not(feature = "toml"),
34 feature = "fig-lang"
35 ))]
36 return EmbedType::FrontmatterFig;
37}
38
39pub enum MetaEditor {
42 Fenced(Embed),
44 Whole(fig::Editor),
46}
47
48impl MetaEditor {
49 pub fn open(text: &str, carrier: MetaCarrier) -> Result<Self> {
51 Ok(match carrier {
52 MetaCarrier::Fenced(kind) => MetaEditor::Fenced(Embed::open(text.as_bytes(), kind)?),
53 MetaCarrier::WholeFile(format) => {
54 MetaEditor::Whole(fig::Editor::open(text.as_bytes(), format)?)
55 }
56 })
57 }
58
59 pub fn open_or_init(text: &str, carrier: Option<MetaCarrier>) -> Result<Self> {
65 Ok(match carrier {
66 Some(MetaCarrier::WholeFile(format)) => {
67 MetaEditor::Whole(fig::Editor::open(text.as_bytes(), format)?)
68 }
69 Some(MetaCarrier::Fenced(kind)) => {
70 MetaEditor::Fenced(Embed::open_or_init(text.as_bytes(), kind)?)
71 }
72 None => MetaEditor::Fenced(Embed::open_or_init(text.as_bytes(), default_embed_type())?),
73 })
74 }
75
76 pub fn set_value(&mut self, path: &[Segment], value: impl Into<fig::Value>) -> Result<()> {
78 match self {
79 MetaEditor::Fenced(e) => e.set_value(path, value)?,
80 MetaEditor::Whole(e) => e.set_value(path, value)?,
81 }
82 Ok(())
83 }
84
85 pub fn replace_value(&mut self, path: &[Segment], value: impl Into<fig::Value>) -> Result<()> {
87 match self {
88 MetaEditor::Fenced(e) => e.replace_value(path, value)?,
89 MetaEditor::Whole(e) => e.replace_value(path, value)?,
90 }
91 Ok(())
92 }
93
94 pub fn replace_key(&mut self, path: &[Segment], key: &str) -> Result<()> {
96 match self {
97 MetaEditor::Fenced(e) => e.replace_key(path, key)?,
98 MetaEditor::Whole(e) => e.replace_key(path, key)?,
99 }
100 Ok(())
101 }
102
103 pub fn append_value(&mut self, path: &[Segment], value: impl Into<fig::Value>) -> Result<()> {
105 match self {
106 MetaEditor::Fenced(e) => e.append_value(path, value)?,
107 MetaEditor::Whole(e) => e.append_value(path, value)?,
108 }
109 Ok(())
110 }
111
112 pub fn delete(&mut self, path: &[Segment]) -> Result<()> {
114 match self {
115 MetaEditor::Fenced(e) => e.delete(path)?,
116 MetaEditor::Whole(e) => e.delete(path)?,
117 }
118 Ok(())
119 }
120
121 pub fn remove_item(&mut self, path: &[Segment], index: usize) -> Result<()> {
123 match self {
124 MetaEditor::Fenced(e) => e.remove_item(path, index)?,
125 MetaEditor::Whole(e) => e.remove_item(path, index)?,
126 }
127 Ok(())
128 }
129
130 pub fn reorder_keys<S: AsRef<str>>(&mut self, path: &[Segment], keys: &[S]) -> Result<()> {
135 match self {
136 MetaEditor::Fenced(e) => e.reorder_keys(path, keys)?,
137 MetaEditor::Whole(e) => e.reorder_keys(path, keys)?,
138 }
139 Ok(())
140 }
141
142 pub fn reorder_items(&mut self, path: &[Segment], indices: &[usize]) -> Result<()> {
147 match self {
148 MetaEditor::Fenced(e) => e.reorder_items(path, indices)?,
149 MetaEditor::Whole(e) => e.reorder_items(path, indices)?,
150 }
151 Ok(())
152 }
153
154 pub fn render(&mut self) -> Result<String> {
156 Ok(match self {
157 MetaEditor::Fenced(e) => e.render()?.to_string(),
158 MetaEditor::Whole(e) => e.source()?.to_string(),
159 })
160 }
161}
162
163pub fn key_path(dotted: &str) -> Vec<Segment<'_>> {
166 dotted
167 .split('.')
168 .map(|part| match part.parse::<usize>() {
169 Ok(index) => Segment::Index(index),
170 Err(_) => Segment::Key(part),
171 })
172 .collect()
173}
174
175pub fn infer_scalar(s: &str) -> fig::Value {
191 match s {
192 "true" => fig::Value::Bool(true),
193 "false" => fig::Value::Bool(false),
194 "null" | "~" => fig::Value::Null,
195 _ if is_zero_padded(s) => fig::Value::Str(s.to_string()),
196 _ => {
197 if let Ok(i) = s.parse::<i64>() {
198 fig::Value::Int(i)
199 } else if let Ok(f) = s.parse::<f64>() {
200 fig::Value::Float(f)
201 } else {
202 fig::Value::Str(s.to_string())
203 }
204 }
205 }
206}
207
208fn is_zero_padded(s: &str) -> bool {
212 let mut digits = s.strip_prefix(['-', '+']).unwrap_or(s).chars();
213 digits.next() == Some('0') && digits.next().is_some_and(|c| c.is_ascii_digit())
214}
215
216pub fn set_in_text(
220 text: &str,
221 carrier: Option<MetaCarrier>,
222 dotted: &str,
223 value: fig::Value,
224) -> Result<String> {
225 let mut editor = MetaEditor::open_or_init(text, carrier)?;
226 let path = key_path(dotted);
227 match path.last() {
228 Some(Segment::Index(_)) => editor.replace_value(&path, value)?,
231 _ => editor.set_value(&path, value)?,
232 }
233 editor.render()
234}
235
236pub fn set_meta_in_text(
265 text: &str,
266 carrier: Option<MetaCarrier>,
267 dotted: &str,
268 value: &prov_graph::meta::Value,
269) -> Result<String> {
270 set_in_text(text, carrier, dotted, fig::Value::from(value))
271}
272
273#[cfg(test)]
276fn value_at<'a>(
277 meta: &'a prov_graph::meta::Value,
278 dotted: &str,
279) -> Option<&'a prov_graph::meta::Value> {
280 let mut current = meta;
281 for part in dotted.split('.') {
282 current = current.as_mapping()?.get(part)?;
283 }
284 Some(current)
285}
286
287pub fn unset_in_text(text: &str, carrier: Option<MetaCarrier>, dotted: &str) -> Result<String> {
291 let carrier = carrier
292 .ok_or_else(|| Error::Structure("document has no embedded metadata block".into()))?;
293 let mut editor = MetaEditor::open(text, carrier)?;
294 editor.delete(&key_path(dotted))?;
295 editor.render()
296}
297
298pub fn reformat_block(body: &str, mapping: &Mapping, target: EmbedType) -> Result<String> {
321 let rendered = Embed::open_or_init(body.as_bytes(), target)?
327 .render()?
328 .to_string();
329 retype_block(&rendered, target, mapping, target)
330}
331
332pub fn retype_block(
345 text: &str,
346 from: EmbedType,
347 mapping: &Mapping,
348 target: EmbedType,
349) -> Result<String> {
350 let mut inner = prov_graph::meta::serialize_mapping(mapping, target.inner_format())?;
351 if !inner.ends_with('\n') {
355 inner.push('\n');
356 }
357 Ok(Embed::retype(text, from, target, &inner)?)
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 fn carrier_of(path: &str, text: &str) -> Option<MetaCarrier> {
365 prov_graph::document::Document::parse(path, text)
366 .unwrap()
367 .carrier
368 }
369
370 #[cfg(feature = "yaml")]
371 #[test]
372 fn set_preserves_comments_and_format() {
373 let text = "---\n# keep me\ntitle: Old\n---\nbody\n";
374 let out =
375 set_in_text(text, carrier_of("x.md", text), "title", infer_scalar("New")).unwrap();
376 assert_eq!(out, "---\n# keep me\ntitle: New\n---\nbody\n");
377 }
378
379 #[cfg(feature = "yaml")]
380 #[test]
381 fn an_all_digit_id_survives_the_round_trip_as_a_string() {
382 let text = "---\ntitle: T\n---\nbody\n";
386 let out = set_in_text(
387 text,
388 carrier_of("x.md", text),
389 "id",
390 fig::Value::Str("0123456".into()),
391 )
392 .unwrap();
393 let back = prov_graph::document::Document::parse("x.md", &out).unwrap();
394 assert_eq!(
395 back.meta.get("id").and_then(prov_graph::Value::as_str),
396 Some("0123456"),
397 "{out}"
398 );
399 }
400
401 #[cfg(feature = "fig-lang")]
402 #[test]
403 fn set_in_a_fig_block_stays_fig() {
404 let text = "```fig\ntitle = prov\n```\nbody\n";
405 let out = set_in_text(
406 text,
407 carrier_of("x.md", text),
408 "title",
409 infer_scalar("renamed"),
410 )
411 .unwrap();
412 assert!(out.starts_with("```fig\n"), "fence preserved: {out}");
413 assert!(
414 out.contains("title = renamed"),
415 "fig dialect preserved: {out}"
416 );
417 assert!(out.ends_with("```\nbody\n"));
418 }
419
420 #[cfg(feature = "yaml")]
421 #[test]
422 fn set_edits_a_bare_config_document() {
423 let text = "# workspace registry\ntitle: ID registry\nregistry:\n abc: a.md\n";
424 let out = set_in_text(
425 text,
426 carrier_of("registry.yaml", text),
427 "registry.abc",
428 infer_scalar("moved/a.md"),
429 )
430 .unwrap();
431 assert!(out.contains("# workspace registry"), "comment kept: {out}");
432 assert!(out.contains("abc: moved/a.md"), "{out}");
433 assert!(!out.contains("---"), "no fences grown: {out}");
434 }
435
436 #[cfg(feature = "yaml")]
437 #[test]
438 fn set_creates_a_block_when_none_exists() {
439 let out = set_in_text("just a body\n", None, "title", infer_scalar("T")).unwrap();
440 assert!(out.starts_with("---\ntitle: T\n---\n"), "{out}");
441 assert!(out.ends_with("just a body\n"));
442 }
443
444 #[cfg(feature = "yaml")]
445 #[test]
446 fn unset_removes_only_the_named_key() {
447 let text = "---\ntitle: T\ndraft: true\n---\nbody\n";
448 let out = unset_in_text(text, carrier_of("x.md", text), "draft").unwrap();
449 assert_eq!(out, "---\ntitle: T\n---\nbody\n");
450 assert!(unset_in_text("no meta\n", None, "x").is_err());
451 }
452
453 #[test]
454 fn scalars_are_inferred() {
455 assert_eq!(infer_scalar("true"), fig::Value::Bool(true));
456 assert_eq!(infer_scalar("42"), fig::Value::Int(42));
457 assert_eq!(infer_scalar("4.5"), fig::Value::Float(4.5));
458 assert_eq!(infer_scalar("null"), fig::Value::Null);
459 assert_eq!(infer_scalar("hello"), fig::Value::Str("hello".into()));
460 }
461
462 #[test]
463 fn zero_padding_survives_inference() {
464 assert_eq!(infer_scalar("08"), fig::Value::Str("08".into()));
468 assert_eq!(infer_scalar("007"), fig::Value::Str("007".into()));
469 assert_eq!(infer_scalar("-04"), fig::Value::Str("-04".into()));
470 assert_eq!(infer_scalar("00"), fig::Value::Str("00".into()));
471
472 assert_eq!(infer_scalar("0"), fig::Value::Int(0));
475 assert_eq!(infer_scalar("0.5"), fig::Value::Float(0.5));
476 assert_eq!(infer_scalar("-0.25"), fig::Value::Float(-0.25));
477 }
478
479 #[cfg(feature = "yaml")]
480 #[test]
481 fn dotted_paths_mix_keys_and_indices() {
482 let text = "---\ncontents:\n- a.md\n- b.md\n---\n";
483 let out = set_in_text(
484 text,
485 carrier_of("x.md", text),
486 "contents.1",
487 infer_scalar("c.md"),
488 )
489 .unwrap();
490 assert!(out.contains("- a.md\n- c.md"), "{out}");
491 }
492
493 #[cfg(feature = "yaml")]
496 #[test]
497 fn replace_key_renames_the_key_and_preserves_comments_elsewhere() {
498 let text = "---\n# keep me\ntitle: Old\nauthor: me\n---\nbody\n";
499 let mut editor = MetaEditor::open(text, carrier_of("x.md", text).unwrap()).unwrap();
500 editor.replace_key(&key_path("title"), "name").unwrap();
501 let out = editor.render().unwrap();
502 assert!(out.contains("name: Old"), "{out}");
503 assert!(!out.contains("title:"), "{out}");
504 assert!(out.contains("# keep me"), "comment lost: {out}");
505 assert!(out.contains("author: me"), "{out}");
506 }
507
508 #[cfg(feature = "yaml")]
509 #[test]
510 fn reorder_keys_moves_listed_keys_first_and_preserves_comments() {
511 let text = "---\n# c1\ntitle: T\n# c2\nauthor: me\ndraft: true\n---\nbody\n";
512 let mut editor = MetaEditor::open(text, carrier_of("x.md", text).unwrap()).unwrap();
513 editor
514 .reorder_keys(&[] as &[Segment], &["draft", "title"])
515 .unwrap();
516 let out = editor.render().unwrap();
517 let draft_pos = out.find("draft:").unwrap();
518 let title_pos = out.find("title:").unwrap();
519 let author_pos = out.find("author:").unwrap();
520 assert!(draft_pos < title_pos && title_pos < author_pos, "{out}");
521 assert!(out.contains("# c1"), "comment lost: {out}");
522 assert!(out.contains("# c2"), "comment lost: {out}");
523 }
524
525 #[cfg(feature = "yaml")]
526 #[test]
527 fn reorder_items_moves_listed_items_first_and_preserves_comments() {
528 let text = "---\ncontents:\n- a # keep a\n- b # keep b\n- c # keep c\n---\nbody\n";
529 let mut editor = MetaEditor::open(text, carrier_of("x.md", text).unwrap()).unwrap();
530 editor
531 .reorder_items(&key_path("contents"), &[2, 0])
532 .unwrap();
533 let out = editor.render().unwrap();
534 assert!(out.contains("# keep a"), "comment lost: {out}");
535 assert!(out.contains("# keep b"), "comment lost: {out}");
536 assert!(out.contains("# keep c"), "comment lost: {out}");
537 let a_pos = out.find("- a").unwrap();
538 let b_pos = out.find("- b").unwrap();
539 let c_pos = out.find("- c").unwrap();
540 assert!(c_pos < a_pos && a_pos < b_pos, "{out}");
542 }
543
544 #[cfg(feature = "yaml")]
545 #[test]
546 fn reorder_keys_works_on_a_whole_file_config_document() {
547 let text =
550 "# workspace registry\ntitle: ID registry\npart_of: index.md\nregistry:\n abc: a.md\n";
551 let mut editor =
552 MetaEditor::open(text, carrier_of("registry.yaml", text).unwrap()).unwrap();
553 editor
554 .reorder_keys(&[] as &[Segment], &["part_of"])
555 .unwrap();
556 let out = editor.render().unwrap();
557 let part_of_pos = out.find("part_of:").unwrap();
558 let title_pos = out.find("title:").unwrap();
559 assert!(part_of_pos < title_pos, "{out}");
560 assert!(out.contains("# workspace registry"), "comment lost: {out}");
561 }
562
563 #[cfg(feature = "yaml")]
568 #[test]
569 fn a_mapping_lands_at_a_path_whose_ancestors_do_not_exist_yet() {
570 let text = "title: prov config\nspec: 1\n";
571 let carrier = carrier_of("config.yaml", text).unwrap();
572 let mut view = prov_graph::meta::Mapping::new();
573 view.insert(
574 "group".into(),
575 prov_graph::meta::Value::String("date".into()),
576 );
577 view.insert("by".into(), prov_graph::meta::Value::String("year".into()));
578
579 let out = set_meta_in_text(
580 text,
581 Some(carrier),
582 "diaryx.views.daily",
583 &prov_graph::meta::Value::Mapping(view),
584 )
585 .expect("a three-deep mapping into a document with no `diaryx` key");
586
587 let doc = prov_graph::Document::parse(std::path::Path::new("config.yaml"), &out).unwrap();
588 let daily = value_at(&doc.meta, "diaryx.views.daily").expect("the block reads back");
589 let map = daily.as_mapping().expect("a mapping");
590 assert_eq!(
591 map.get("group").and_then(prov_graph::meta::Value::as_str),
592 Some("date")
593 );
594 assert_eq!(
595 map.get("by").and_then(prov_graph::meta::Value::as_str),
596 Some("year")
597 );
598 assert!(
599 out.contains("title: prov config"),
600 "the rest survived: {out}"
601 );
602 }
603
604 #[cfg(feature = "yaml")]
608 #[test]
609 fn replacing_a_mapping_drops_the_keys_it_no_longer_declares() {
610 let text = "title: t\ndiaryx:\n views:\n daily:\n group: date\n under: '[Daily](id:abc)'\n";
611 let carrier = carrier_of("config.yaml", text).unwrap();
612 let mut view = prov_graph::meta::Mapping::new();
613 view.insert(
614 "group".into(),
615 prov_graph::meta::Value::String("date".into()),
616 );
617
618 let out = set_meta_in_text(
619 text,
620 Some(carrier),
621 "diaryx.views.daily",
622 &prov_graph::meta::Value::Mapping(view),
623 )
624 .expect("replace");
625
626 let doc = prov_graph::Document::parse(std::path::Path::new("config.yaml"), &out).unwrap();
627 let map = value_at(&doc.meta, "diaryx.views.daily")
628 .and_then(prov_graph::meta::Value::as_mapping)
629 .expect("still a mapping");
630 assert_eq!(
631 map.get("group").and_then(prov_graph::meta::Value::as_str),
632 Some("date")
633 );
634 assert!(
635 map.get("under").is_none(),
636 "a dropped key must not linger: {out}"
637 );
638 }
639
640 #[cfg(feature = "yaml")]
643 #[test]
644 fn a_sibling_mapping_survives_the_write() {
645 let text = "title: t\ndiaryx:\n views:\n daily:\n group: date\n";
646 let carrier = carrier_of("config.yaml", text).unwrap();
647 let mut view = prov_graph::meta::Mapping::new();
648 view.insert(
649 "group".into(),
650 prov_graph::meta::Value::String("people".into()),
651 );
652
653 let out = set_meta_in_text(
654 text,
655 Some(carrier),
656 "diaryx.views.folks",
657 &prov_graph::meta::Value::Mapping(view),
658 )
659 .expect("a second view");
660
661 let doc = prov_graph::Document::parse(std::path::Path::new("config.yaml"), &out).unwrap();
662 assert_eq!(
663 value_at(&doc.meta, "diaryx.views.daily.group")
664 .and_then(prov_graph::meta::Value::as_str),
665 Some("date"),
666 "the first view survived: {out}"
667 );
668 assert_eq!(
669 value_at(&doc.meta, "diaryx.views.folks.group")
670 .and_then(prov_graph::meta::Value::as_str),
671 Some("people")
672 );
673 }
674
675 #[cfg(feature = "yaml")]
677 #[test]
678 fn nested_mappings_flatten_all_the_way_down() {
679 let text = "title: t\n";
680 let carrier = carrier_of("config.yaml", text).unwrap();
681 let mut inner = prov_graph::meta::Mapping::new();
682 inner.insert("closed".into(), prov_graph::meta::Value::Bool(true));
683 let mut outer = prov_graph::meta::Mapping::new();
684 outer.insert("audience".into(), prov_graph::meta::Value::Mapping(inner));
685
686 let out = set_meta_in_text(
687 text,
688 Some(carrier),
689 "a.b",
690 &prov_graph::meta::Value::Mapping(outer),
691 )
692 .expect("nested write");
693 let doc = prov_graph::Document::parse(std::path::Path::new("config.yaml"), &out).unwrap();
694 assert_eq!(
695 value_at(&doc.meta, "a.b.audience.closed"),
696 Some(&prov_graph::meta::Value::Bool(true)),
697 "{out}"
698 );
699 }
700
701 #[cfg(feature = "yaml")]
704 #[test]
705 fn a_sequence_lands_under_a_parent_that_already_exists() {
706 let text = "title: t\nvocab:\n audience:\n closed: true\n";
707 let carrier = carrier_of("config.yaml", text).unwrap();
708 let mut inner = prov_graph::meta::Mapping::new();
709 inner.insert(
710 "terms".into(),
711 prov_graph::meta::Value::Sequence(vec![
712 prov_graph::meta::Value::String("public".into()),
713 prov_graph::meta::Value::String("private".into()),
714 ]),
715 );
716 let mut outer = prov_graph::meta::Mapping::new();
717 outer.insert("audience".into(), prov_graph::meta::Value::Mapping(inner));
718
719 let out = set_meta_in_text(
720 text,
721 Some(carrier),
722 "vocab",
723 &prov_graph::meta::Value::Mapping(outer),
724 )
725 .expect("a sequence under an existing block");
726 let doc = prov_graph::Document::parse(std::path::Path::new("config.yaml"), &out).unwrap();
727 let terms = value_at(&doc.meta, "vocab.audience.terms")
728 .and_then(prov_graph::meta::Value::as_sequence)
729 .expect("the sequence reads back as a sequence");
730 assert_eq!(terms.len(), 2, "{out}");
731 }
732
733 #[cfg(feature = "yaml")]
740 #[test]
741 fn a_sequence_lands_as_a_sequence_at_a_path_with_no_parent_block() {
742 let text = "title: t\n";
743 let carrier = carrier_of("config.yaml", text).unwrap();
744 let mut outer = prov_graph::meta::Mapping::new();
745 outer.insert(
746 "terms".into(),
747 prov_graph::meta::Value::Sequence(vec![prov_graph::meta::Value::String(
748 "public".into(),
749 )]),
750 );
751
752 let out = set_meta_in_text(
753 text,
754 Some(carrier),
755 "deep.nested",
756 &prov_graph::meta::Value::Mapping(outer),
757 )
758 .expect("a list with no parent block now lands");
759 let doc = prov_graph::Document::parse(std::path::Path::new("config.yaml"), &out).unwrap();
760 let terms = value_at(&doc.meta, "deep.nested.terms")
761 .and_then(prov_graph::meta::Value::as_sequence)
762 .unwrap_or_else(|| panic!("the list must read back as a list: {out}"));
763 assert_eq!(terms.len(), 1, "{out}");
764 assert_eq!(terms[0].as_str(), Some("public"), "{out}");
765 }
766
767 #[cfg(feature = "yaml")]
772 #[test]
773 fn writing_a_mapping_replaces_the_subtree_rather_than_merging_into_it() {
774 let text = "title: t\na:\n keep: 1\n stale: 9\n gone:\n deeper: 2\n";
775 let carrier = carrier_of("config.yaml", text).unwrap();
776 let mut fresh = prov_graph::meta::Mapping::new();
777 fresh.insert("keep".into(), prov_graph::meta::Value::String("new".into()));
778
779 let out = set_meta_in_text(
780 text,
781 Some(carrier),
782 "a",
783 &prov_graph::meta::Value::Mapping(fresh),
784 )
785 .expect("replace an existing block");
786 let doc = prov_graph::Document::parse(std::path::Path::new("config.yaml"), &out).unwrap();
787 assert_eq!(
788 value_at(&doc.meta, "a.keep").and_then(prov_graph::meta::Value::as_str),
789 Some("new"),
790 "{out}"
791 );
792 assert!(value_at(&doc.meta, "a.stale").is_none(), "{out}");
793 assert!(value_at(&doc.meta, "a.gone").is_none(), "{out}");
794 assert_eq!(
796 value_at(&doc.meta, "title").and_then(prov_graph::meta::Value::as_str),
797 Some("t"),
798 "{out}"
799 );
800 }
801
802 #[cfg(feature = "yaml")]
807 #[test]
808 fn retyping_a_mid_document_island_keeps_the_head_above_it() {
809 let text = concat!(
810 "<!doctype html>\n",
811 "<html><head><title>KEEP ME</title></head>\n",
812 "<body>\n",
813 "<script type=\"application/yaml\">\n",
814 "title: Mid\n",
815 "</script>\n",
816 "<p>tail</p>\n",
817 "</html>\n",
818 );
819 let doc = prov_graph::document::Document::parse("page.html", text).unwrap();
820 let mapping = doc.meta.as_mapping().unwrap();
821 let out = retype_block(
822 text,
823 EmbedType::HtmlScriptYaml,
824 mapping,
825 EmbedType::HtmlCodeYaml,
826 )
827 .unwrap();
828
829 let head = out.find("KEEP ME").expect("the head survived");
830 let island = out
831 .find("<pre")
832 .expect("the block was re-housed as html_code");
833 assert!(head < island, "the head must stay above the block:\n{out}");
834 assert!(out.contains("<p>tail</p>"), "the tail survived:\n{out}");
835 assert!(
836 !out.contains("<script"),
837 "the old archetype is gone:\n{out}"
838 );
839 }
840}