about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEtomicBomb <ethan@ethan.ws>2024-07-26 17:09:32 +0000
committerEtomicBomb <ethan@ethan.ws>2024-08-07 18:48:03 +0000
commit4b418cd4aa9b2b8c52e8029c13b0530abf97bce7 (patch)
tree8434166c2fba88573e3f9420fcbfead02a1745d9
parent67663fc680428cf22267f974106d2805008a8568 (diff)
downloadrust-4b418cd4aa9b2b8c52e8029c13b0530abf97bce7.tar.gz
rust-4b418cd4aa9b2b8c52e8029c13b0530abf97bce7.zip
rename sortedjson -> orderedjson
-rw-r--r--src/librustdoc/Cargo.toml2
-rw-r--r--src/librustdoc/html/render/mod.rs2
-rw-r--r--src/librustdoc/html/render/ordered_json.rs (renamed from src/librustdoc/html/render/sorted_json.rs)56
-rw-r--r--src/librustdoc/html/render/ordered_json/tests.rs (renamed from src/librustdoc/html/render/sorted_json/tests.rs)52
-rw-r--r--src/librustdoc/html/render/search_index.rs10
-rw-r--r--src/librustdoc/html/render/sorted_template.rs108
-rw-r--r--src/librustdoc/html/render/sorted_template/tests.rs21
-rw-r--r--src/librustdoc/html/render/write_shared.rs141
-rw-r--r--src/librustdoc/html/render/write_shared/tests.rs29
9 files changed, 228 insertions, 193 deletions
diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml
index 67ba8c77317..b3fccbf6456 100644
--- a/src/librustdoc/Cargo.toml
+++ b/src/librustdoc/Cargo.toml
@@ -16,7 +16,7 @@ minifier = "0.3.0"
 pulldown-cmark-old = { version = "0.9.6", package = "pulldown-cmark", default-features = false }
 regex = "1"
 rustdoc-json-types = { path = "../rustdoc-json-types" }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_json = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 smallvec = "1.8.1"
 tempfile = "3"
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 4b1c9b4af47..586c3c509b4 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -29,9 +29,9 @@ pub(crate) mod search_index;
 mod tests;
 
 mod context;
+mod ordered_json;
 mod print_item;
 pub(crate) mod sidebar;
-mod sorted_json;
 mod sorted_template;
 mod span_map;
 mod type_layout;
diff --git a/src/librustdoc/html/render/sorted_json.rs b/src/librustdoc/html/render/ordered_json.rs
index e937382f5b0..3f76ff659d0 100644
--- a/src/librustdoc/html/render/sorted_json.rs
+++ b/src/librustdoc/html/render/ordered_json.rs
@@ -1,72 +1,74 @@
+use std::borrow::Borrow;
+use std::fmt;
+
 use itertools::Itertools as _;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use std::borrow::Borrow;
-use std::fmt;
 
 /// Prerenedered json.
 ///
-/// Arrays are sorted by their stringified entries, and objects are sorted by their stringified
-/// keys.
-///
-/// Must use serde_json with the preserve_order feature.
-///
 /// Both the Display and serde_json::to_string implementations write the serialized json
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
 #[serde(from = "Value")]
 #[serde(into = "Value")]
-pub(crate) struct SortedJson(String);
+pub(crate) struct OrderedJson(String);
 
-impl SortedJson {
+impl OrderedJson {
     /// If you pass in an array, it will not be sorted.
-    pub(crate) fn serialize<T: Serialize>(item: T) -> Self {
-        SortedJson(serde_json::to_string(&item).unwrap())
+    pub(crate) fn serialize<T: Serialize>(item: T) -> Result<Self, serde_json::Error> {
+        Ok(OrderedJson(serde_json::to_string(&item)?))
     }
 
     /// Serializes and sorts
-    pub(crate) fn array<T: Borrow<SortedJson>, I: IntoIterator<Item = T>>(items: I) -> Self {
+    pub(crate) fn array_sorted<T: Borrow<OrderedJson>, I: IntoIterator<Item = T>>(
+        items: I,
+    ) -> Self {
         let items = items
             .into_iter()
             .sorted_unstable_by(|a, b| a.borrow().cmp(&b.borrow()))
             .format_with(",", |item, f| f(item.borrow()));
-        SortedJson(format!("[{}]", items))
+        OrderedJson(format!("[{}]", items))
     }
 
-    pub(crate) fn array_unsorted<T: Borrow<SortedJson>, I: IntoIterator<Item = T>>(
+    pub(crate) fn array_unsorted<T: Borrow<OrderedJson>, I: IntoIterator<Item = T>>(
         items: I,
     ) -> Self {
         let items = items.into_iter().format_with(",", |item, f| f(item.borrow()));
-        SortedJson(format!("[{items}]"))
+        OrderedJson(format!("[{items}]"))
     }
 }
 
-impl fmt::Display for SortedJson {
+impl fmt::Display for OrderedJson {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.0)
+        self.0.fmt(f)
     }
 }
 
-impl From<Value> for SortedJson {
+impl From<Value> for OrderedJson {
     fn from(value: Value) -> Self {
-        SortedJson(serde_json::to_string(&value).unwrap())
+        let serialized =
+            serde_json::to_string(&value).expect("Serializing a Value to String should never fail");
+        OrderedJson(serialized)
     }
 }
 
-impl From<SortedJson> for Value {
-    fn from(json: SortedJson) -> Self {
-        serde_json::from_str(&json.0).unwrap()
+impl From<OrderedJson> for Value {
+    fn from(json: OrderedJson) -> Self {
+        serde_json::from_str(&json.0).expect("OrderedJson should always store valid JSON")
     }
 }
 
 /// For use in JSON.parse('{...}').
 ///
-/// JSON.parse supposedly loads faster than raw JS source,
+/// Assumes we are going to be wrapped in single quoted strings.
+///
+/// JSON.parse loads faster than raw JS source,
 /// so this is used for large objects.
 #[derive(Debug, Clone, Serialize, Deserialize)]
-pub(crate) struct EscapedJson(SortedJson);
+pub(crate) struct EscapedJson(OrderedJson);
 
-impl From<SortedJson> for EscapedJson {
-    fn from(json: SortedJson) -> Self {
+impl From<OrderedJson> for EscapedJson {
+    fn from(json: OrderedJson) -> Self {
         EscapedJson(json)
     }
 }
@@ -77,7 +79,7 @@ impl fmt::Display for EscapedJson {
         // for JSON content.
         // We need to escape double quotes for the JSON
         let json = self.0.0.replace('\\', r"\\").replace('\'', r"\'").replace("\\\"", "\\\\\"");
-        write!(f, "{}", json)
+        json.fmt(f)
     }
 }
 
diff --git a/src/librustdoc/html/render/sorted_json/tests.rs b/src/librustdoc/html/render/ordered_json/tests.rs
index 1e72c6f614c..e0fe6446b9a 100644
--- a/src/librustdoc/html/render/sorted_json/tests.rs
+++ b/src/librustdoc/html/render/ordered_json/tests.rs
@@ -1,90 +1,90 @@
-use super::super::sorted_json::*;
+use super::super::ordered_json::*;
 
-fn check(json: SortedJson, serialized: &str) {
+fn check(json: OrderedJson, serialized: &str) {
     assert_eq!(json.to_string(), serialized);
     assert_eq!(serde_json::to_string(&json).unwrap(), serialized);
 
     let json = json.to_string();
-    let json: SortedJson = serde_json::from_str(&json).unwrap();
+    let json: OrderedJson = serde_json::from_str(&json).unwrap();
 
     assert_eq!(json.to_string(), serialized);
     assert_eq!(serde_json::to_string(&json).unwrap(), serialized);
 
     let json = serde_json::to_string(&json).unwrap();
-    let json: SortedJson = serde_json::from_str(&json).unwrap();
+    let json: OrderedJson = serde_json::from_str(&json).unwrap();
 
     assert_eq!(json.to_string(), serialized);
     assert_eq!(serde_json::to_string(&json).unwrap(), serialized);
 }
 
-// Test this basic are needed because we are testing that our Display impl + serialize impl don't
-// nest everything in extra level of string. We also are testing round trip.
+// Make sure there is no extra level of string, plus number of escapes.
 #[test]
 fn escape_json_number() {
-    let json = SortedJson::serialize(3);
+    let json = OrderedJson::serialize(3).unwrap();
     let json = EscapedJson::from(json);
     assert_eq!(format!("{json}"), "3");
 }
 
 #[test]
 fn escape_json_single_quote() {
-    let json = SortedJson::serialize("he's");
+    let json = OrderedJson::serialize("he's").unwrap();
     let json = EscapedJson::from(json);
     assert_eq!(format!("{json}"), r#""he\'s""#);
 }
 
 #[test]
 fn escape_json_array() {
-    let json = SortedJson::serialize([1, 2, 3]);
+    let json = OrderedJson::serialize([1, 2, 3]).unwrap();
     let json = EscapedJson::from(json);
     assert_eq!(format!("{json}"), r#"[1,2,3]"#);
 }
 
 #[test]
 fn escape_json_string() {
-    let json = SortedJson::serialize(r#"he"llo"#);
+    let json = OrderedJson::serialize(r#"he"llo"#).unwrap();
     let json = EscapedJson::from(json);
     assert_eq!(format!("{json}"), r#""he\\\"llo""#);
 }
 
 #[test]
 fn escape_json_string_escaped() {
-    let json = SortedJson::serialize(r#"he\"llo"#);
+    let json = OrderedJson::serialize(r#"he\"llo"#).unwrap();
     let json = EscapedJson::from(json);
     assert_eq!(format!("{json}"), r#""he\\\\\\\"llo""#);
 }
 
 #[test]
 fn escape_json_string_escaped_escaped() {
-    let json = SortedJson::serialize(r#"he\\"llo"#);
+    let json = OrderedJson::serialize(r#"he\\"llo"#).unwrap();
     let json = EscapedJson::from(json);
     assert_eq!(format!("{json}"), r#""he\\\\\\\\\\\"llo""#);
 }
 
+// Testing round trip + making sure there is no extra level of string
 #[test]
 fn number() {
-    let json = SortedJson::serialize(3);
+    let json = OrderedJson::serialize(3).unwrap();
     let serialized = "3";
     check(json, serialized);
 }
 
 #[test]
 fn boolean() {
-    let json = SortedJson::serialize(true);
+    let json = OrderedJson::serialize(true).unwrap();
     let serialized = "true";
     check(json, serialized);
 }
 
 #[test]
 fn string() {
-    let json = SortedJson::serialize("he\"llo");
+    let json = OrderedJson::serialize("he\"llo").unwrap();
     let serialized = r#""he\"llo""#;
     check(json, serialized);
 }
 
 #[test]
 fn serialize_array() {
-    let json = SortedJson::serialize([3, 1, 2]);
+    let json = OrderedJson::serialize([3, 1, 2]).unwrap();
     let serialized = "[3,1,2]";
     check(json, serialized);
 }
@@ -93,18 +93,19 @@ fn serialize_array() {
 fn sorted_array() {
     let items = ["c", "a", "b"];
     let serialized = r#"["a","b","c"]"#;
-    let items: Vec<SortedJson> = items.into_iter().map(SortedJson::serialize).collect();
-    let json = SortedJson::array(items);
+    let items: Vec<OrderedJson> =
+        items.into_iter().map(OrderedJson::serialize).collect::<Result<Vec<_>, _>>().unwrap();
+    let json = OrderedJson::array_sorted(items);
     check(json, serialized);
 }
 
 #[test]
 fn nested_array() {
-    let a = SortedJson::serialize(3);
-    let b = SortedJson::serialize(2);
-    let c = SortedJson::serialize(1);
-    let d = SortedJson::serialize([1, 3, 2]);
-    let json = SortedJson::array([a, b, c, d]);
+    let a = OrderedJson::serialize(3).unwrap();
+    let b = OrderedJson::serialize(2).unwrap();
+    let c = OrderedJson::serialize(1).unwrap();
+    let d = OrderedJson::serialize([1, 3, 2]).unwrap();
+    let json = OrderedJson::array_sorted([a, b, c, d]);
     let serialized = r#"[1,2,3,[1,3,2]]"#;
     check(json, serialized);
 }
@@ -113,7 +114,8 @@ fn nested_array() {
 fn array_unsorted() {
     let items = ["c", "a", "b"];
     let serialized = r#"["c","a","b"]"#;
-    let items: Vec<SortedJson> = items.into_iter().map(SortedJson::serialize).collect();
-    let json = SortedJson::array_unsorted(items);
+    let items: Vec<OrderedJson> =
+        items.into_iter().map(OrderedJson::serialize).collect::<Result<Vec<_>, _>>().unwrap();
+    let json = OrderedJson::array_unsorted(items);
     check(json, serialized);
 }
diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs
index 184e5afba3c..8a12bdef69b 100644
--- a/src/librustdoc/html/render/search_index.rs
+++ b/src/librustdoc/html/render/search_index.rs
@@ -18,7 +18,7 @@ use crate::formats::cache::{Cache, OrphanImplItem};
 use crate::formats::item_type::ItemType;
 use crate::html::format::join_with_double_colon;
 use crate::html::markdown::short_markdown_summary;
-use crate::html::render::sorted_json::SortedJson;
+use crate::html::render::ordered_json::OrderedJson;
 use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, RenderTypeId};
 
 /// The serialized search description sharded version
@@ -47,7 +47,7 @@ use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, Re
 /// [2]: https://en.wikipedia.org/wiki/Sliding_window_protocol#Basic_concept
 /// [3]: https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/description-tcp-features
 pub(crate) struct SerializedSearchIndex {
-    pub(crate) index: SortedJson,
+    pub(crate) index: OrderedJson,
     pub(crate) desc: Vec<(usize, String)>,
 }
 
@@ -693,9 +693,9 @@ pub(crate) fn build_index<'tcx>(
         desc_index,
         empty_desc,
     };
-    let index = SortedJson::array_unsorted([
-        SortedJson::serialize(crate_name.as_str()),
-        SortedJson::serialize(data),
+    let index = OrderedJson::array_unsorted([
+        OrderedJson::serialize(crate_name.as_str()).unwrap(),
+        OrderedJson::serialize(data).unwrap(),
     ]);
     SerializedSearchIndex { index, desc }
 }
diff --git a/src/librustdoc/html/render/sorted_template.rs b/src/librustdoc/html/render/sorted_template.rs
index 8e0a2ee0fd4..1dc70408f01 100644
--- a/src/librustdoc/html/render/sorted_template.rs
+++ b/src/librustdoc/html/render/sorted_template.rs
@@ -1,8 +1,9 @@
 use std::collections::BTreeSet;
-use std::fmt;
+use std::fmt::{self, Write as _};
 use std::marker::PhantomData;
 use std::str::FromStr;
 
+use itertools::{Itertools as _, Position};
 use serde::{Deserialize, Serialize};
 
 /// Append-only templates for sorted, deduplicated lists of items.
@@ -13,7 +14,7 @@ pub(crate) struct SortedTemplate<F> {
     format: PhantomData<F>,
     before: String,
     after: String,
-    contents: BTreeSet<String>,
+    fragments: BTreeSet<String>,
 }
 
 /// Written to last line of file to specify the location of each fragment
@@ -22,82 +23,88 @@ struct Offset {
     /// Index of the first byte in the template
     start: usize,
     /// The length of each fragment in the encoded template, including the separator
-    delta: Vec<usize>,
+    fragment_lengths: Vec<usize>,
 }
 
 impl<F> SortedTemplate<F> {
     /// Generate this template from arbitary text.
     /// Will insert wherever the substring `magic` can be found.
     /// Errors if it does not appear exactly once.
-    pub(crate) fn magic(template: &str, magic: &str) -> Result<Self, Error> {
-        let mut split = template.split(magic);
-        let before = split.next().ok_or(Error)?;
-        let after = split.next().ok_or(Error)?;
+    pub(crate) fn from_template(template: &str, delimiter: &str) -> Result<Self, Error> {
+        let mut split = template.split(delimiter);
+        let before = split.next().ok_or(Error("delimiter should appear at least once"))?;
+        let after = split.next().ok_or(Error("delimiter should appear at least once"))?;
+        // not `split_once` because we want to check for too many occurrences
         if split.next().is_some() {
-            return Err(Error);
+            return Err(Error("delimiter should appear at most once"));
         }
-        Ok(Self::before_after(before, after))
+        Ok(Self::from_before_after(before, after))
     }
 
-    /// Template will insert contents between `before` and `after`
-    pub(crate) fn before_after<S: ToString, T: ToString>(before: S, after: T) -> Self {
+    /// Template will insert fragments between `before` and `after`
+    pub(crate) fn from_before_after<S: ToString, T: ToString>(before: S, after: T) -> Self {
         let before = before.to_string();
         let after = after.to_string();
-        SortedTemplate { format: PhantomData, before, after, contents: Default::default() }
+        SortedTemplate { format: PhantomData, before, after, fragments: Default::default() }
     }
 }
 
-impl<F: FileFormat> SortedTemplate<F> {
+impl<F> SortedTemplate<F> {
     /// Adds this text to the template
     pub(crate) fn append(&mut self, insert: String) {
-        self.contents.insert(insert);
+        self.fragments.insert(insert);
     }
 }
 
 impl<F: FileFormat> fmt::Display for SortedTemplate<F> {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let mut delta = Vec::default();
+    fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let mut fragment_lengths = Vec::default();
         write!(f, "{}", self.before)?;
-        let contents: Vec<_> = self.contents.iter().collect();
-        let mut sep = "";
-        for content in contents {
-            delta.push(sep.len() + content.len());
-            write!(f, "{}{}", sep, content)?;
-            sep = F::SEPARATOR;
+        for (p, fragment) in self.fragments.iter().with_position() {
+            let mut f = DeltaWriter { inner: &mut f, delta: 0 };
+            let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
+            write!(f, "{}{}", sep, fragment)?;
+            fragment_lengths.push(f.delta);
         }
-        let offset = Offset { start: self.before.len(), delta };
+        let offset = Offset { start: self.before.len(), fragment_lengths };
         let offset = serde_json::to_string(&offset).unwrap();
-        write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)?;
-        Ok(())
+        write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)
     }
 }
 
-fn checked_split_at(s: &str, index: usize) -> Option<(&str, &str)> {
-    s.is_char_boundary(index).then(|| s.split_at(index))
-}
-
 impl<F: FileFormat> FromStr for SortedTemplate<F> {
     type Err = Error;
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let (s, offset) = s.rsplit_once("\n").ok_or(Error)?;
-        let offset = offset.strip_prefix(F::COMMENT_START).ok_or(Error)?;
-        let offset = offset.strip_suffix(F::COMMENT_END).ok_or(Error)?;
-        let offset: Offset = serde_json::from_str(&offset).map_err(|_| Error)?;
-        let (before, mut s) = checked_split_at(s, offset.start).ok_or(Error)?;
-        let mut contents = BTreeSet::default();
-        let mut sep = "";
-        for &index in offset.delta.iter() {
-            let (content, rest) = checked_split_at(s, index).ok_or(Error)?;
+        let (s, offset) = s
+            .rsplit_once("\n")
+            .ok_or(Error("invalid format: should have a newline on the last line"))?;
+        let offset = offset
+            .strip_prefix(F::COMMENT_START)
+            .ok_or(Error("last line expected to start with a comment"))?;
+        let offset = offset
+            .strip_suffix(F::COMMENT_END)
+            .ok_or(Error("last line expected to end with a comment"))?;
+        let offset: Offset = serde_json::from_str(&offset).map_err(|_| {
+            Error("could not find insertion location descriptor object on last line")
+        })?;
+        let (before, mut s) =
+            s.split_at_checked(offset.start).ok_or(Error("invalid start: out of bounds"))?;
+        let mut fragments = BTreeSet::default();
+        for (p, &index) in offset.fragment_lengths.iter().with_position() {
+            let (fragment, rest) =
+                s.split_at_checked(index).ok_or(Error("invalid fragment length: out of bounds"))?;
             s = rest;
-            let content = content.strip_prefix(sep).ok_or(Error)?;
-            contents.insert(content.to_string());
-            sep = F::SEPARATOR;
+            let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
+            let fragment = fragment
+                .strip_prefix(sep)
+                .ok_or(Error("invalid fragment length: expected to find separator here"))?;
+            fragments.insert(fragment.to_string());
         }
         Ok(SortedTemplate {
             format: PhantomData,
             before: before.to_string(),
             after: s.to_string(),
-            contents,
+            fragments,
         })
     }
 }
@@ -127,11 +134,24 @@ impl FileFormat for Js {
 }
 
 #[derive(Debug, Clone)]
-pub(crate) struct Error;
+pub(crate) struct Error(&'static str);
 
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "invalid template")
+        write!(f, "invalid template: {}", self.0)
+    }
+}
+
+struct DeltaWriter<W> {
+    inner: W,
+    delta: usize,
+}
+
+impl<W: fmt::Write> fmt::Write for DeltaWriter<W> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        self.inner.write_str(s)?;
+        self.delta += s.len();
+        Ok(())
     }
 }
 
diff --git a/src/librustdoc/html/render/sorted_template/tests.rs b/src/librustdoc/html/render/sorted_template/tests.rs
index 04553f65a21..db057463005 100644
--- a/src/librustdoc/html/render/sorted_template/tests.rs
+++ b/src/librustdoc/html/render/sorted_template/tests.rs
@@ -1,6 +1,7 @@
-use super::super::sorted_template::*;
 use std::str::FromStr;
 
+use super::super::sorted_template::*;
+
 fn is_comment_js(s: &str) -> bool {
     s.starts_with("//")
 }
@@ -13,7 +14,7 @@ fn is_comment_html(s: &str) -> bool {
 #[test]
 fn html_from_empty() {
     let inserts = ["<p>hello</p>", "<p>kind</p>", "<p>hello</p>", "<p>world</p>"];
-    let mut template = SortedTemplate::<Html>::before_after("", "");
+    let mut template = SortedTemplate::<Html>::from_before_after("", "");
     for insert in inserts {
         template.append(insert.to_string());
     }
@@ -29,7 +30,7 @@ fn html_page() {
     let inserts = ["<p>hello</p>", "<p>kind</p>", "<p>world</p>"];
     let before = "<html><head></head><body>";
     let after = "</body>";
-    let mut template = SortedTemplate::<Html>::before_after(before, after);
+    let mut template = SortedTemplate::<Html>::from_before_after(before, after);
     for insert in inserts {
         template.append(insert.to_string());
     }
@@ -43,7 +44,7 @@ fn html_page() {
 #[test]
 fn js_from_empty() {
     let inserts = ["1", "2", "2", "2", "3", "1"];
-    let mut template = SortedTemplate::<Js>::before_after("", "");
+    let mut template = SortedTemplate::<Js>::from_before_after("", "");
     for insert in inserts {
         template.append(insert.to_string());
     }
@@ -56,7 +57,7 @@ fn js_from_empty() {
 
 #[test]
 fn js_empty_array() {
-    let template = SortedTemplate::<Js>::before_after("[", "]");
+    let template = SortedTemplate::<Js>::from_before_after("[", "]");
     let template = format!("{template}");
     let (template, end) = template.rsplit_once("\n").unwrap();
     assert_eq!(template, format!("[]"));
@@ -67,7 +68,7 @@ fn js_empty_array() {
 #[test]
 fn js_number_array() {
     let inserts = ["1", "2", "3"];
-    let mut template = SortedTemplate::<Js>::before_after("[", "]");
+    let mut template = SortedTemplate::<Js>::from_before_after("[", "]");
     for insert in inserts {
         template.append(insert.to_string());
     }
@@ -81,7 +82,7 @@ fn js_number_array() {
 #[test]
 fn magic_js_number_array() {
     let inserts = ["1", "1"];
-    let mut template = SortedTemplate::<Js>::magic("[#]", "#").unwrap();
+    let mut template = SortedTemplate::<Js>::from_template("[#]", "#").unwrap();
     for insert in inserts {
         template.append(insert.to_string());
     }
@@ -95,7 +96,7 @@ fn magic_js_number_array() {
 #[test]
 fn round_trip_js() {
     let inserts = ["1", "2", "3"];
-    let mut template = SortedTemplate::<Js>::before_after("[", "]");
+    let mut template = SortedTemplate::<Js>::from_before_after("[", "]");
     for insert in inserts {
         template.append(insert.to_string());
     }
@@ -114,7 +115,7 @@ fn round_trip_html() {
     let inserts = ["<p>hello</p>", "<p>kind</p>", "<p>world</p>", "<p>kind</p>"];
     let before = "<html><head></head><body>";
     let after = "</body>";
-    let mut template = SortedTemplate::<Html>::before_after(before, after);
+    let mut template = SortedTemplate::<Html>::from_before_after(before, after);
     template.append(inserts[0].to_string());
     template.append(inserts[1].to_string());
     let template = format!("{template}");
@@ -129,7 +130,7 @@ fn round_trip_html() {
 #[test]
 fn blank_js() {
     let inserts = ["1", "2", "3"];
-    let template = SortedTemplate::<Js>::before_after("", "");
+    let template = SortedTemplate::<Js>::from_before_after("", "");
     let template = format!("{template}");
     let (t, _) = template.rsplit_once("\n").unwrap();
     assert_eq!(t, "");
diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs
index c2d2b4cd7d9..ef3c35c20ab 100644
--- a/src/librustdoc/html/render/write_shared.rs
+++ b/src/librustdoc/html/render/write_shared.rs
@@ -1,4 +1,4 @@
-//! Rustdoc writes out two kinds of shared files:
+//! Rustdoc writes aut two kinds of shared files:
 //!  - Static files, which are embedded in the rustdoc binary and are written with a
 //!    filename that includes a hash of their contents. These will always have a new
 //!    URL if the contents change, so they are safe to cache with the
@@ -13,18 +13,16 @@
 //!    --resource-suffix flag and are emitted when --emit-type is empty (default)
 //!    or contains "invocation-specific".
 
-use std::any::Any;
 use std::cell::RefCell;
 use std::ffi::OsString;
 use std::fs::File;
-use std::io::BufWriter;
-use std::io::Write as _;
+use std::io::{self, BufWriter, Write as _};
 use std::iter::once;
 use std::marker::PhantomData;
 use std::path::{Component, Path, PathBuf};
 use std::rc::{Rc, Weak};
 use std::str::FromStr;
-use std::{fmt, fs, io};
+use std::{fmt, fs};
 
 use indexmap::IndexMap;
 use itertools::Itertools;
@@ -35,8 +33,9 @@ use rustc_middle::ty::fast_reject::{DeepRejectCtxt, TreatParams};
 use rustc_middle::ty::TyCtxt;
 use rustc_span::def_id::DefId;
 use rustc_span::Symbol;
+use serde::de::DeserializeOwned;
 use serde::ser::SerializeSeq;
-use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer};
+use serde::{Deserialize, Serialize, Serializer};
 
 use super::{collect_paths_for_type, ensure_trailing_slash, Context, RenderMode};
 use crate::clean::{Crate, Item, ItemId, ItemKind};
@@ -48,9 +47,8 @@ use crate::formats::item_type::ItemType;
 use crate::formats::Impl;
 use crate::html::format::Buffer;
 use crate::html::layout;
-use crate::html::render::search_index::build_index;
-use crate::html::render::search_index::SerializedSearchIndex;
-use crate::html::render::sorted_json::{EscapedJson, SortedJson};
+use crate::html::render::ordered_json::{EscapedJson, OrderedJson};
+use crate::html::render::search_index::{build_index, SerializedSearchIndex};
 use crate::html::render::sorted_template::{self, FileFormat, SortedTemplate};
 use crate::html::render::{AssocItemLink, ImplRenderingParameters};
 use crate::html::static_files::{self, suffix_path};
@@ -76,7 +74,7 @@ pub(crate) fn write_shared(
 
     let crate_name = krate.name(cx.tcx());
     let crate_name = crate_name.as_str(); // rand
-    let crate_name_json = SortedJson::serialize(crate_name); // "rand"
+    let crate_name_json = OrderedJson::serialize(crate_name).unwrap(); // "rand"
     let external_crates = hack_get_external_crate_names(&cx.dst)?;
     let info = CrateInfo {
         src_files_js: SourcesPart::get(cx, &crate_name_json)?,
@@ -111,11 +109,7 @@ pub(crate) fn write_shared(
             );
         }
         None if opt.enable_index_page => {
-            write_rendered_cci::<CratesIndexPart, _>(
-                || CratesIndexPart::blank(cx),
-                dst,
-                &crates,
-            )?;
+            write_rendered_cci::<CratesIndexPart, _>(|| CratesIndexPart::blank(cx), dst, &crates)?;
         }
         _ => {} // they don't want an index page
     }
@@ -171,9 +165,9 @@ fn write_search_desc(
     search_desc: &[(usize, String)],
 ) -> Result<(), Error> {
     let crate_name = krate.name(cx.tcx()).to_string();
-    let encoded_crate_name = SortedJson::serialize(&crate_name);
+    let encoded_crate_name = OrderedJson::serialize(&crate_name).unwrap();
     let path = PathBuf::from_iter([&cx.dst, Path::new("search.desc"), Path::new(&crate_name)]);
-    if Path::new(&path).exists() {
+    if path.exists() {
         try_err!(fs::remove_dir_all(&path), &path);
     }
     for (i, (_, part)) in search_desc.iter().enumerate() {
@@ -182,7 +176,7 @@ fn write_search_desc(
             &cx.shared.resource_suffix,
         );
         let path = path.join(filename);
-        let part = SortedJson::serialize(&part);
+        let part = OrderedJson::serialize(&part).unwrap();
         let part = format!("searchState.loadedDescShard({encoded_crate_name}, {i}, {part})");
         create_parents(&path)?;
         try_err!(fs::write(&path, part), &path);
@@ -201,20 +195,6 @@ struct CrateInfo {
     type_impl: PartsAndLocations<TypeAliasPart>,
 }
 
-impl CrateInfo {
-    /// Gets a reference to the cross-crate information parts for `T`
-    fn get<T: CciPart>(&self) -> &PartsAndLocations<T> {
-        (&self.src_files_js as &dyn Any)
-            .downcast_ref()
-            .or_else(|| (&self.search_index_js as &dyn Any).downcast_ref())
-            .or_else(|| (&self.all_crates as &dyn Any).downcast_ref())
-            .or_else(|| (&self.crates_index as &dyn Any).downcast_ref())
-            .or_else(|| (&self.trait_impl as &dyn Any).downcast_ref())
-            .or_else(|| (&self.type_impl as &dyn Any).downcast_ref())
-            .expect("this should be an exhaustive list of `CciPart`s")
-    }
-}
-
 /// Paths (relative to the doc root) and their pre-merge contents
 #[derive(Serialize, Deserialize, Debug, Clone)]
 #[serde(transparent)]
@@ -263,6 +243,7 @@ impl<T, U: fmt::Display> fmt::Display for Part<T, U> {
 trait CciPart: Sized + fmt::Display + DeserializeOwned + 'static {
     /// Identifies the file format of the cross-crate information
     type FileFormat: sorted_template::FileFormat;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self>;
 }
 
 #[derive(Serialize, Deserialize, Clone, Default, Debug)]
@@ -270,11 +251,14 @@ struct SearchIndex;
 type SearchIndexPart = Part<SearchIndex, EscapedJson>;
 impl CciPart for SearchIndexPart {
     type FileFormat = sorted_template::Js;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.search_index_js
+    }
 }
 
 impl SearchIndexPart {
     fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
-        SortedTemplate::before_after(
+        SortedTemplate::from_before_after(
             r"var searchIndex = new Map(JSON.parse('[",
             r"]'));
 if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
@@ -283,7 +267,7 @@ else if (window.initSearch) window.initSearch(searchIndex);",
     }
 
     fn get(
-        search_index: SortedJson,
+        search_index: OrderedJson,
         resource_suffix: &str,
     ) -> Result<PartsAndLocations<Self>, Error> {
         let path = suffix_path("search-index.js", resource_suffix);
@@ -294,17 +278,20 @@ else if (window.initSearch) window.initSearch(searchIndex);",
 
 #[derive(Serialize, Deserialize, Clone, Default, Debug)]
 struct AllCrates;
-type AllCratesPart = Part<AllCrates, SortedJson>;
+type AllCratesPart = Part<AllCrates, OrderedJson>;
 impl CciPart for AllCratesPart {
     type FileFormat = sorted_template::Js;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.all_crates
+    }
 }
 
 impl AllCratesPart {
     fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
-        SortedTemplate::before_after("window.ALL_CRATES = [", "];")
+        SortedTemplate::from_before_after("window.ALL_CRATES = [", "];")
     }
 
-    fn get(crate_name_json: SortedJson) -> Result<PartsAndLocations<Self>, Error> {
+    fn get(crate_name_json: OrderedJson) -> Result<PartsAndLocations<Self>, Error> {
         // external hack_get_external_crate_names not needed here, because
         // there's no way that we write the search index but not crates.js
         let path = PathBuf::from("crates.js");
@@ -339,6 +326,9 @@ struct CratesIndex;
 type CratesIndexPart = Part<CratesIndex, String>;
 impl CciPart for CratesIndexPart {
     type FileFormat = sorted_template::Html;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.crates_index
+    }
 }
 
 impl CratesIndexPart {
@@ -354,10 +344,11 @@ impl CratesIndexPart {
         };
         let layout = &cx.shared.layout;
         let style_files = &cx.shared.style_files;
-        const MAGIC: &str = "\u{FFFC}"; // users are being naughty if they have this
-        let content = format!("<h1>List of all crates</h1><ul class=\"all-items\">{MAGIC}</ul>");
+        const DELIMITER: &str = "\u{FFFC}"; // users are being naughty if they have this
+        let content =
+            format!("<h1>List of all crates</h1><ul class=\"all-items\">{DELIMITER}</ul>");
         let template = layout::render(layout, &page, "", content, &style_files);
-        match SortedTemplate::magic(&template, MAGIC) {
+        match SortedTemplate::from_template(&template, DELIMITER) {
             Ok(template) => template,
             Err(e) => panic!(
                 "Object Replacement Character (U+FFFC) should not appear in the --index-page: {e}"
@@ -385,6 +376,9 @@ struct Sources;
 type SourcesPart = Part<Sources, EscapedJson>;
 impl CciPart for SourcesPart {
     type FileFormat = sorted_template::Js;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.src_files_js
+    }
 }
 
 impl SourcesPart {
@@ -392,14 +386,14 @@ impl SourcesPart {
         // This needs to be `var`, not `const`.
         // This variable needs declared in the current global scope so that if
         // src-script.js loads first, it can pick it up.
-        SortedTemplate::before_after(
+        SortedTemplate::from_before_after(
             r"var srcIndex = new Map(JSON.parse('[",
             r"]'));
 createSrcSidebar();",
         )
     }
 
-    fn get(cx: &Context<'_>, crate_name: &SortedJson) -> Result<PartsAndLocations<Self>, Error> {
+    fn get(cx: &Context<'_>, crate_name: &OrderedJson) -> Result<PartsAndLocations<Self>, Error> {
         let hierarchy = Rc::new(Hierarchy::default());
         cx.shared
             .local_sources
@@ -408,7 +402,7 @@ createSrcSidebar();",
             .for_each(|source| hierarchy.add_path(source));
         let path = suffix_path("src-files.js", &cx.shared.resource_suffix);
         let hierarchy = hierarchy.to_json_string();
-        let part = SortedJson::array_unsorted([crate_name, &hierarchy]);
+        let part = OrderedJson::array_unsorted([crate_name, &hierarchy]);
         let part = EscapedJson::from(part);
         Ok(PartsAndLocations::with(path, part))
     }
@@ -428,21 +422,23 @@ impl Hierarchy {
         Self { elem, parent: Rc::downgrade(parent), ..Self::default() }
     }
 
-    fn to_json_string(&self) -> SortedJson {
+    fn to_json_string(&self) -> OrderedJson {
         let subs = self.children.borrow();
         let files = self.elems.borrow();
-        let name = SortedJson::serialize(self.elem.to_str().expect("invalid osstring conversion"));
+        let name = OrderedJson::serialize(self.elem.to_str().expect("invalid osstring conversion"))
+            .unwrap();
         let mut out = Vec::from([name]);
         if !subs.is_empty() || !files.is_empty() {
             let subs = subs.iter().map(|(_, s)| s.to_json_string());
-            out.push(SortedJson::array(subs));
+            out.push(OrderedJson::array_sorted(subs));
         }
         if !files.is_empty() {
-            let files =
-                files.iter().map(|s| SortedJson::serialize(s.to_str().expect("invalid osstring")));
-            out.push(SortedJson::array(files));
+            let files = files
+                .iter()
+                .map(|s| OrderedJson::serialize(s.to_str().expect("invalid osstring")).unwrap());
+            out.push(OrderedJson::array_sorted(files));
         }
-        SortedJson::array_unsorted(out)
+        OrderedJson::array_unsorted(out)
     }
 
     fn add_path(self: &Rc<Self>, path: &Path) {
@@ -481,14 +477,17 @@ impl Hierarchy {
 
 #[derive(Serialize, Deserialize, Clone, Default, Debug)]
 struct TypeAlias;
-type TypeAliasPart = Part<TypeAlias, SortedJson>;
+type TypeAliasPart = Part<TypeAlias, OrderedJson>;
 impl CciPart for TypeAliasPart {
     type FileFormat = sorted_template::Js;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.type_impl
+    }
 }
 
 impl TypeAliasPart {
     fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
-        SortedTemplate::before_after(
+        SortedTemplate::from_before_after(
             r"(function() {
     var type_impls = Object.fromEntries([",
             r"]);
@@ -504,7 +503,7 @@ impl TypeAliasPart {
     fn get(
         cx: &mut Context<'_>,
         krate: &Crate,
-        crate_name_json: &SortedJson,
+        crate_name_json: &OrderedJson,
     ) -> Result<PartsAndLocations<Self>, Error> {
         let cache = &Rc::clone(&cx.shared).cache;
         let mut path_parts = PartsAndLocations::default();
@@ -594,9 +593,10 @@ impl TypeAliasPart {
                 aliased_type.target_fqp[aliased_type.target_fqp.len() - 1]
             ));
 
-            let part =
-                SortedJson::array(impls.iter().map(SortedJson::serialize).collect::<Vec<_>>());
-            path_parts.push(path, SortedJson::array_unsorted([crate_name_json, &part]));
+            let part = OrderedJson::array_sorted(
+                impls.iter().map(OrderedJson::serialize).collect::<Result<Vec<_>, _>>().unwrap(),
+            );
+            path_parts.push(path, OrderedJson::array_unsorted([crate_name_json, &part]));
         }
         Ok(path_parts)
     }
@@ -604,14 +604,17 @@ impl TypeAliasPart {
 
 #[derive(Serialize, Deserialize, Clone, Default, Debug)]
 struct TraitAlias;
-type TraitAliasPart = Part<TraitAlias, SortedJson>;
+type TraitAliasPart = Part<TraitAlias, OrderedJson>;
 impl CciPart for TraitAliasPart {
     type FileFormat = sorted_template::Js;
+    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
+        &crate_info.trait_impl
+    }
 }
 
 impl TraitAliasPart {
     fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
-        SortedTemplate::before_after(
+        SortedTemplate::from_before_after(
             r"(function() {
     var implementors = Object.fromEntries([",
             r"]);
@@ -626,7 +629,7 @@ impl TraitAliasPart {
 
     fn get(
         cx: &mut Context<'_>,
-        crate_name_json: &SortedJson,
+        crate_name_json: &OrderedJson,
     ) -> Result<PartsAndLocations<Self>, Error> {
         let cache = &cx.shared.cache;
         let mut path_parts = PartsAndLocations::default();
@@ -688,10 +691,14 @@ impl TraitAliasPart {
             }
             path.push(&format!("{remote_item_type}.{}.js", remote_path[remote_path.len() - 1]));
 
-            let part = SortedJson::array(
-                implementors.iter().map(SortedJson::serialize).collect::<Vec<_>>(),
+            let part = OrderedJson::array_sorted(
+                implementors
+                    .iter()
+                    .map(OrderedJson::serialize)
+                    .collect::<Result<Vec<_>, _>>()
+                    .unwrap(),
             );
-            path_parts.push(path, SortedJson::array_unsorted([crate_name_json, &part]));
+            path_parts.push(path, OrderedJson::array_unsorted([crate_name_json, &part]));
         }
         Ok(path_parts)
     }
@@ -864,13 +871,15 @@ fn get_path_parts<T: CciPart>(
     crates_info: &[CrateInfo],
 ) -> FxHashMap<PathBuf, Vec<String>> {
     let mut templates: FxHashMap<PathBuf, Vec<String>> = FxHashMap::default();
-    crates_info.iter().map(|crate_info| crate_info.get::<T>().parts.iter()).flatten().for_each(
-        |(path, part)| {
+    crates_info
+        .iter()
+        .map(|crate_info| T::from_crate_info(crate_info).parts.iter())
+        .flatten()
+        .for_each(|(path, part)| {
             let path = dst.join(&path);
             let part = part.to_string();
             templates.entry(path).or_default().push(part);
-        },
-    );
+        });
     templates
 }
 
diff --git a/src/librustdoc/html/render/write_shared/tests.rs b/src/librustdoc/html/render/write_shared/tests.rs
index 000e233aec0..4d1874b7df5 100644
--- a/src/librustdoc/html/render/write_shared/tests.rs
+++ b/src/librustdoc/html/render/write_shared/tests.rs
@@ -1,4 +1,4 @@
-use crate::html::render::sorted_json::{EscapedJson, SortedJson};
+use crate::html::render::ordered_json::{EscapedJson, OrderedJson};
 use crate::html::render::sorted_template::{Html, SortedTemplate};
 use crate::html::render::write_shared::*;
 
@@ -26,13 +26,13 @@ fn sources_template() {
         r"var srcIndex = new Map(JSON.parse('[]'));
 createSrcSidebar();"
     );
-    template.append(EscapedJson::from(SortedJson::serialize("u")).to_string());
+    template.append(EscapedJson::from(OrderedJson::serialize("u").unwrap()).to_string());
     assert_eq!(
         but_last_line(&template.to_string()),
         r#"var srcIndex = new Map(JSON.parse('["u"]'));
 createSrcSidebar();"#
     );
-    template.append(EscapedJson::from(SortedJson::serialize("v")).to_string());
+    template.append(EscapedJson::from(OrderedJson::serialize("v").unwrap()).to_string());
     assert_eq!(
         but_last_line(&template.to_string()),
         r#"var srcIndex = new Map(JSON.parse('["u","v"]'));
@@ -42,7 +42,8 @@ createSrcSidebar();"#
 
 #[test]
 fn sources_parts() {
-    let parts = SearchIndexPart::get(SortedJson::serialize(["foo", "bar"]), "suffix").unwrap();
+    let parts =
+        SearchIndexPart::get(OrderedJson::serialize(["foo", "bar"]).unwrap(), "suffix").unwrap();
     assert_eq!(&parts.parts[0].0, Path::new("search-indexsuffix.js"));
     assert_eq!(&parts.parts[0].1.to_string(), r#"["foo","bar"]"#);
 }
@@ -51,15 +52,15 @@ fn sources_parts() {
 fn all_crates_template() {
     let mut template = AllCratesPart::blank();
     assert_eq!(but_last_line(&template.to_string()), r"window.ALL_CRATES = [];");
-    template.append(EscapedJson::from(SortedJson::serialize("b")).to_string());
+    template.append(EscapedJson::from(OrderedJson::serialize("b").unwrap()).to_string());
     assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["b"];"#);
-    template.append(EscapedJson::from(SortedJson::serialize("a")).to_string());
+    template.append(EscapedJson::from(OrderedJson::serialize("a").unwrap()).to_string());
     assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["a","b"];"#);
 }
 
 #[test]
 fn all_crates_parts() {
-    let parts = AllCratesPart::get(SortedJson::serialize("crate")).unwrap();
+    let parts = AllCratesPart::get(OrderedJson::serialize("crate").unwrap()).unwrap();
     assert_eq!(&parts.parts[0].0, Path::new("crates.js"));
     assert_eq!(&parts.parts[0].1.to_string(), r#""crate""#);
 }
@@ -73,14 +74,14 @@ fn search_index_template() {
 if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
 else if (window.initSearch) window.initSearch(searchIndex);"
     );
-    template.append(EscapedJson::from(SortedJson::serialize([1, 2])).to_string());
+    template.append(EscapedJson::from(OrderedJson::serialize([1, 2]).unwrap()).to_string());
     assert_eq!(
         but_last_line(&template.to_string()),
         r"var searchIndex = new Map(JSON.parse('[[1,2]]'));
 if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
 else if (window.initSearch) window.initSearch(searchIndex);"
     );
-    template.append(EscapedJson::from(SortedJson::serialize([4, 3])).to_string());
+    template.append(EscapedJson::from(OrderedJson::serialize([4, 3]).unwrap()).to_string());
     assert_eq!(
         but_last_line(&template.to_string()),
         r"var searchIndex = new Map(JSON.parse('[[1,2],[4,3]]'));
@@ -119,7 +120,7 @@ fn trait_alias_template() {
     }
 })()"#,
     );
-    template.append(SortedJson::serialize(["a"]).to_string());
+    template.append(OrderedJson::serialize(["a"]).unwrap().to_string());
     assert_eq!(
         but_last_line(&template.to_string()),
         r#"(function() {
@@ -131,7 +132,7 @@ fn trait_alias_template() {
     }
 })()"#,
     );
-    template.append(SortedJson::serialize(["b"]).to_string());
+    template.append(OrderedJson::serialize(["b"]).unwrap().to_string());
     assert_eq!(
         but_last_line(&template.to_string()),
         r#"(function() {
@@ -159,7 +160,7 @@ fn type_alias_template() {
     }
 })()"#,
     );
-    template.append(SortedJson::serialize(["a"]).to_string());
+    template.append(OrderedJson::serialize(["a"]).unwrap().to_string());
     assert_eq!(
         but_last_line(&template.to_string()),
         r#"(function() {
@@ -171,7 +172,7 @@ fn type_alias_template() {
     }
 })()"#,
     );
-    template.append(SortedJson::serialize(["b"]).to_string());
+    template.append(OrderedJson::serialize(["b"]).unwrap().to_string());
     assert_eq!(
         but_last_line(&template.to_string()),
         r#"(function() {
@@ -189,7 +190,7 @@ fn type_alias_template() {
 fn read_template_test() {
     let path = tempfile::TempDir::new().unwrap();
     let path = path.path().join("file.html");
-    let make_blank = || SortedTemplate::<Html>::before_after("<div>", "</div>");
+    let make_blank = || SortedTemplate::<Html>::from_before_after("<div>", "</div>");
 
     let template = read_template_or_blank(make_blank, &path).unwrap();
     assert_eq!(but_last_line(&template.to_string()), "<div></div>");