Auto merge of #128252 - EtomicBomb:pre-rfc, r=notriddle

modularize rustdoc's write_shared

Refactor src/librustdoc/html/render/write_shared.rs to reduce code duplication, adding unit tests

* Extract + unit test code for sorting and rendering JSON, which is duplicated 9 times in the current impl
* Extract + unit test code for encoding JSON as single quoted strings, which is duplicated twice in the current impl
* Unit tests for cross-crate information file formats
* Generic interface to add new kinds of cross-crate information files in the future
* Intended to match current behavior exactly, except for a merge info comment it adds to the bottom of cci files
* This PR is intended to reduce the review burden from my [mergeable rustdoc rfc](https://github.com/rust-lang/rfcs/pull/3662) implementation PR, which is a [small commit based on this branch](https://github.com/EtomicBomb/rust/tree/rfc). This code is agnostic to the RFC and does not include any of the flags discussed there, but cleanly enables the addition of these flags in the future because it is more modular
This commit is contained in:
bors 2024-08-20 20:23:29 +00:00
commit 5aea14073e
9 changed files with 1599 additions and 731 deletions

View file

@ -14,7 +14,6 @@ use rustc_span::edition::Edition;
use rustc_span::{sym, FileName, Symbol};
use super::print_item::{full_path, item_path, print_item};
use super::search_index::build_index;
use super::sidebar::{print_sidebar, sidebar_module_like, Sidebar};
use super::write_shared::write_shared;
use super::{collect_spans_and_sources, scrape_examples_help, AllTypes, LinkFromSrc, StylePath};
@ -573,13 +572,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
}
if !no_emit_shared {
// Build our search index
let index = build_index(&krate, &mut Rc::get_mut(&mut cx.shared).unwrap().cache, tcx);
// Write shared runs within a flock; disable thread dispatching of IO temporarily.
Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(true);
write_shared(&mut cx, &krate, index, &md_opts)?;
Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(false);
write_shared(&mut cx, &krate, &md_opts, tcx)?;
}
Ok((cx, krate))

View file

@ -29,8 +29,10 @@ pub(crate) mod search_index;
mod tests;
mod context;
mod ordered_json;
mod print_item;
pub(crate) mod sidebar;
mod sorted_template;
mod span_map;
mod type_layout;
mod write_shared;

View file

@ -0,0 +1,83 @@
use std::borrow::Borrow;
use std::fmt;
use itertools::Itertools as _;
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// Prerendered json.
///
/// 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 OrderedJson(String);
impl OrderedJson {
/// If you pass in an array, it will not be sorted.
pub(crate) fn serialize<T: Serialize>(item: T) -> Result<Self, serde_json::Error> {
Ok(Self(serde_json::to_string(&item)?))
}
/// Serializes and sorts
pub(crate) fn array_sorted<T: Borrow<Self>, 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()));
Self(format!("[{}]", items))
}
pub(crate) fn array_unsorted<T: Borrow<Self>, I: IntoIterator<Item = T>>(items: I) -> Self {
let items = items.into_iter().format_with(",", |item, f| f(item.borrow()));
Self(format!("[{items}]"))
}
}
impl fmt::Display for OrderedJson {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<Value> for OrderedJson {
fn from(value: Value) -> Self {
let serialized =
serde_json::to_string(&value).expect("Serializing a Value to String should never fail");
Self(serialized)
}
}
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('{...}').
///
/// 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(OrderedJson);
impl From<OrderedJson> for EscapedJson {
fn from(json: OrderedJson) -> Self {
Self(json)
}
}
impl fmt::Display for EscapedJson {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// All these `replace` calls are because we have to go through JS string
// for JSON content.
// We need to escape double quotes for the JSON
let json = self.0.0.replace('\\', r"\\").replace('\'', r"\'").replace("\\\"", "\\\\\"");
json.fmt(f)
}
}
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,121 @@
use super::super::ordered_json::*;
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: 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: OrderedJson = serde_json::from_str(&json).unwrap();
assert_eq!(json.to_string(), serialized);
assert_eq!(serde_json::to_string(&json).unwrap(), serialized);
}
// Make sure there is no extra level of string, plus number of escapes.
#[test]
fn escape_json_number() {
let json = OrderedJson::serialize(3).unwrap();
let json = EscapedJson::from(json);
assert_eq!(format!("{json}"), "3");
}
#[test]
fn escape_json_single_quote() {
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 = 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 = 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 = 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 = 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 = OrderedJson::serialize(3).unwrap();
let serialized = "3";
check(json, serialized);
}
#[test]
fn boolean() {
let json = OrderedJson::serialize(true).unwrap();
let serialized = "true";
check(json, serialized);
}
#[test]
fn string() {
let json = OrderedJson::serialize("he\"llo").unwrap();
let serialized = r#""he\"llo""#;
check(json, serialized);
}
#[test]
fn serialize_array() {
let json = OrderedJson::serialize([3, 1, 2]).unwrap();
let serialized = "[3,1,2]";
check(json, serialized);
}
#[test]
fn sorted_array() {
let items = ["c", "a", "b"];
let serialized = r#"["a","b","c"]"#;
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 = 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);
}
#[test]
fn array_unsorted() {
let items = ["c", "a", "b"];
let serialized = r#"["c","a","b"]"#;
let items: Vec<OrderedJson> =
items.into_iter().map(OrderedJson::serialize).collect::<Result<Vec<_>, _>>().unwrap();
let json = OrderedJson::array_unsorted(items);
check(json, serialized);
}

View file

@ -18,6 +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::ordered_json::OrderedJson;
use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, RenderTypeId};
/// The serialized search description sharded version
@ -46,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: String,
pub(crate) index: OrderedJson,
pub(crate) desc: Vec<(usize, String)>,
}
@ -683,24 +684,19 @@ pub(crate) fn build_index<'tcx>(
// The index, which is actually used to search, is JSON
// It uses `JSON.parse(..)` to actually load, since JSON
// parses faster than the full JavaScript syntax.
let index = format!(
r#"["{}",{}]"#,
krate.name(tcx),
serde_json::to_string(&CrateData {
items: crate_items,
paths: crate_paths,
aliases: &aliases,
associated_item_disambiguators: &associated_item_disambiguators,
desc_index,
empty_desc,
})
.expect("failed serde conversion")
// All these `replace` calls are because we have to go through JS string for JSON content.
.replace('\\', r"\\")
.replace('\'', r"\'")
// We need to escape double quotes for the JSON.
.replace("\\\"", "\\\\\"")
);
let crate_name = krate.name(tcx);
let data = CrateData {
items: crate_items,
paths: crate_paths,
aliases: &aliases,
associated_item_disambiguators: &associated_item_disambiguators,
desc_index,
empty_desc,
};
let index = OrderedJson::array_unsorted([
OrderedJson::serialize(crate_name.as_str()).unwrap(),
OrderedJson::serialize(data).unwrap(),
]);
SerializedSearchIndex { index, desc }
}

View file

@ -0,0 +1,159 @@
use std::collections::BTreeSet;
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.
///
/// Last line of the rendered output is a comment encoding the next insertion point.
#[derive(Debug, Clone)]
pub(crate) struct SortedTemplate<F> {
format: PhantomData<F>,
before: String,
after: String,
fragments: BTreeSet<String>,
}
/// Written to last line of file to specify the location of each fragment
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Offset {
/// Index of the first byte in the template
start: usize,
/// The length of each fragment in the encoded template, including the separator
fragment_lengths: Vec<usize>,
}
impl<F> SortedTemplate<F> {
/// Generate this template from arbitary text.
/// Will insert wherever the substring `delimiter` can be found.
/// Errors if it does not appear exactly once.
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("delimiter should appear at most once"));
}
Ok(Self::from_before_after(before, after))
}
/// 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();
Self { format: PhantomData, before, after, fragments: Default::default() }
}
}
impl<F> SortedTemplate<F> {
/// Adds this text to the template
pub(crate) fn append(&mut self, insert: String) {
self.fragments.insert(insert);
}
}
impl<F: FileFormat> fmt::Display for SortedTemplate<F> {
fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut fragment_lengths = Vec::default();
write!(f, "{}", self.before)?;
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(), fragment_lengths };
let offset = serde_json::to_string(&offset).unwrap();
write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)
}
}
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("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 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(Self {
format: PhantomData,
before: before.to_string(),
after: s.to_string(),
fragments,
})
}
}
pub(crate) trait FileFormat {
const COMMENT_START: &'static str;
const COMMENT_END: &'static str;
const SEPARATOR: &'static str;
}
#[derive(Debug, Clone)]
pub(crate) struct Html;
impl FileFormat for Html {
const COMMENT_START: &'static str = "<!--";
const COMMENT_END: &'static str = "-->";
const SEPARATOR: &'static str = "";
}
#[derive(Debug, Clone)]
pub(crate) struct Js;
impl FileFormat for Js {
const COMMENT_START: &'static str = "//";
const COMMENT_END: &'static str = "";
const SEPARATOR: &'static str = ",";
}
#[derive(Debug, Clone)]
pub(crate) struct Error(&'static str);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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(())
}
}
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,149 @@
use std::str::FromStr;
use super::super::sorted_template::*;
fn is_comment_js(s: &str) -> bool {
s.starts_with("//")
}
fn is_comment_html(s: &str) -> bool {
// not correct but good enough for these tests
s.starts_with("<!--") && s.ends_with("-->")
}
#[test]
fn html_from_empty() {
let inserts = ["<p>hello</p>", "<p>kind</p>", "<p>hello</p>", "<p>world</p>"];
let mut template = SortedTemplate::<Html>::from_before_after("", "");
for insert in inserts {
template.append(insert.to_string());
}
let template = format!("{template}");
let (template, end) = template.rsplit_once("\n").unwrap();
assert_eq!(template, "<p>hello</p><p>kind</p><p>world</p>");
assert!(is_comment_html(end));
assert!(!end.contains("\n"));
}
#[test]
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>::from_before_after(before, after);
for insert in inserts {
template.append(insert.to_string());
}
let template = format!("{template}");
let (template, end) = template.rsplit_once("\n").unwrap();
assert_eq!(template, format!("{before}{}{after}", inserts.join("")));
assert!(is_comment_html(end));
assert!(!end.contains("\n"));
}
#[test]
fn js_from_empty() {
let inserts = ["1", "2", "2", "2", "3", "1"];
let mut template = SortedTemplate::<Js>::from_before_after("", "");
for insert in inserts {
template.append(insert.to_string());
}
let template = format!("{template}");
let (template, end) = template.rsplit_once("\n").unwrap();
assert_eq!(template, "1,2,3");
assert!(is_comment_js(end));
assert!(!end.contains("\n"));
}
#[test]
fn js_empty_array() {
let template = SortedTemplate::<Js>::from_before_after("[", "]");
let template = format!("{template}");
let (template, end) = template.rsplit_once("\n").unwrap();
assert_eq!(template, format!("[]"));
assert!(is_comment_js(end));
assert!(!end.contains("\n"));
}
#[test]
fn js_number_array() {
let inserts = ["1", "2", "3"];
let mut template = SortedTemplate::<Js>::from_before_after("[", "]");
for insert in inserts {
template.append(insert.to_string());
}
let template = format!("{template}");
let (template, end) = template.rsplit_once("\n").unwrap();
assert_eq!(template, format!("[1,2,3]"));
assert!(is_comment_js(end));
assert!(!end.contains("\n"));
}
#[test]
fn magic_js_number_array() {
let inserts = ["1", "1"];
let mut template = SortedTemplate::<Js>::from_template("[#]", "#").unwrap();
for insert in inserts {
template.append(insert.to_string());
}
let template = format!("{template}");
let (template, end) = template.rsplit_once("\n").unwrap();
assert_eq!(template, format!("[1]"));
assert!(is_comment_js(end));
assert!(!end.contains("\n"));
}
#[test]
fn round_trip_js() {
let inserts = ["1", "2", "3"];
let mut template = SortedTemplate::<Js>::from_before_after("[", "]");
for insert in inserts {
template.append(insert.to_string());
}
let template1 = format!("{template}");
let mut template = SortedTemplate::<Js>::from_str(&template1).unwrap();
assert_eq!(template1, format!("{template}"));
template.append("4".to_string());
let template = format!("{template}");
let (template, end) = template.rsplit_once("\n").unwrap();
assert_eq!(template, "[1,2,3,4]");
assert!(is_comment_js(end));
}
#[test]
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>::from_before_after(before, after);
template.append(inserts[0].to_string());
template.append(inserts[1].to_string());
let template = format!("{template}");
let mut template = SortedTemplate::<Html>::from_str(&template).unwrap();
template.append(inserts[2].to_string());
let template = format!("{template}");
let (template, end) = template.rsplit_once("\n").unwrap();
assert_eq!(template, format!("{before}<p>hello</p><p>kind</p><p>world</p>{after}"));
assert!(is_comment_html(end));
}
#[test]
fn blank_js() {
let inserts = ["1", "2", "3"];
let template = SortedTemplate::<Js>::from_before_after("", "");
let template = format!("{template}");
let (t, _) = template.rsplit_once("\n").unwrap();
assert_eq!(t, "");
let mut template = SortedTemplate::<Js>::from_str(&template).unwrap();
for insert in inserts {
template.append(insert.to_string());
}
let template1 = format!("{template}");
let mut template = SortedTemplate::<Js>::from_str(&template1).unwrap();
assert_eq!(template1, format!("{template}"));
template.append("4".to_string());
let template = format!("{template}");
let (template, end) = template.rsplit_once("\n").unwrap();
assert_eq!(template, "1,2,3,4");
assert!(is_comment_js(end));
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,207 @@
use crate::html::render::ordered_json::{EscapedJson, OrderedJson};
use crate::html::render::sorted_template::{Html, SortedTemplate};
use crate::html::render::write_shared::*;
#[test]
fn hack_external_crate_names() {
let path = tempfile::TempDir::new().unwrap();
let path = path.path();
let crates = hack_get_external_crate_names(&path).unwrap();
assert!(crates.is_empty());
fs::write(path.join("crates.js"), r#"window.ALL_CRATES = ["a","b","c"];"#).unwrap();
let crates = hack_get_external_crate_names(&path).unwrap();
assert_eq!(crates, ["a".to_string(), "b".to_string(), "c".to_string()]);
}
fn but_last_line(s: &str) -> &str {
let (before, _) = s.rsplit_once("\n").unwrap();
before
}
#[test]
fn sources_template() {
let mut template = SourcesPart::blank();
assert_eq!(
but_last_line(&template.to_string()),
r"var srcIndex = new Map(JSON.parse('[]'));
createSrcSidebar();"
);
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(OrderedJson::serialize("v").unwrap()).to_string());
assert_eq!(
but_last_line(&template.to_string()),
r#"var srcIndex = new Map(JSON.parse('["u","v"]'));
createSrcSidebar();"#
);
}
#[test]
fn sources_parts() {
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"]"#);
}
#[test]
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(OrderedJson::serialize("b").unwrap()).to_string());
assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["b"];"#);
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(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""#);
}
#[test]
fn search_index_template() {
let mut template = SearchIndexPart::blank();
assert_eq!(
but_last_line(&template.to_string()),
r"var searchIndex = new Map(JSON.parse('[]'));
if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
else if (window.initSearch) window.initSearch(searchIndex);"
);
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(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]]'));
if (typeof exports !== 'undefined') exports.searchIndex = searchIndex;
else if (window.initSearch) window.initSearch(searchIndex);"
);
}
#[test]
fn crates_index_part() {
let external_crates = ["bar".to_string(), "baz".to_string()];
let mut parts = CratesIndexPart::get("foo", &external_crates).unwrap();
parts.parts.sort_by(|a, b| a.1.to_string().cmp(&b.1.to_string()));
assert_eq!(&parts.parts[0].0, Path::new("index.html"));
assert_eq!(&parts.parts[0].1.to_string(), r#"<li><a href="bar/index.html">bar</a></li>"#);
assert_eq!(&parts.parts[1].0, Path::new("index.html"));
assert_eq!(&parts.parts[1].1.to_string(), r#"<li><a href="baz/index.html">baz</a></li>"#);
assert_eq!(&parts.parts[2].0, Path::new("index.html"));
assert_eq!(&parts.parts[2].1.to_string(), r#"<li><a href="foo/index.html">foo</a></li>"#);
}
#[test]
fn trait_alias_template() {
let mut template = TraitAliasPart::blank();
assert_eq!(
but_last_line(&template.to_string()),
r#"(function() {
var implementors = Object.fromEntries([]);
if (window.register_implementors) {
window.register_implementors(implementors);
} else {
window.pending_implementors = implementors;
}
})()"#,
);
template.append(OrderedJson::serialize(["a"]).unwrap().to_string());
assert_eq!(
but_last_line(&template.to_string()),
r#"(function() {
var implementors = Object.fromEntries([["a"]]);
if (window.register_implementors) {
window.register_implementors(implementors);
} else {
window.pending_implementors = implementors;
}
})()"#,
);
template.append(OrderedJson::serialize(["b"]).unwrap().to_string());
assert_eq!(
but_last_line(&template.to_string()),
r#"(function() {
var implementors = Object.fromEntries([["a"],["b"]]);
if (window.register_implementors) {
window.register_implementors(implementors);
} else {
window.pending_implementors = implementors;
}
})()"#,
);
}
#[test]
fn type_alias_template() {
let mut template = TypeAliasPart::blank();
assert_eq!(
but_last_line(&template.to_string()),
r#"(function() {
var type_impls = Object.fromEntries([]);
if (window.register_type_impls) {
window.register_type_impls(type_impls);
} else {
window.pending_type_impls = type_impls;
}
})()"#,
);
template.append(OrderedJson::serialize(["a"]).unwrap().to_string());
assert_eq!(
but_last_line(&template.to_string()),
r#"(function() {
var type_impls = Object.fromEntries([["a"]]);
if (window.register_type_impls) {
window.register_type_impls(type_impls);
} else {
window.pending_type_impls = type_impls;
}
})()"#,
);
template.append(OrderedJson::serialize(["b"]).unwrap().to_string());
assert_eq!(
but_last_line(&template.to_string()),
r#"(function() {
var type_impls = Object.fromEntries([["a"],["b"]]);
if (window.register_type_impls) {
window.register_type_impls(type_impls);
} else {
window.pending_type_impls = type_impls;
}
})()"#,
);
}
#[test]
fn read_template_test() {
let path = tempfile::TempDir::new().unwrap();
let path = path.path().join("file.html");
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>");
fs::write(&path, template.to_string()).unwrap();
let mut template = read_template_or_blank(make_blank, &path).unwrap();
template.append("<img/>".to_string());
fs::write(&path, template.to_string()).unwrap();
let mut template = read_template_or_blank(make_blank, &path).unwrap();
template.append("<br/>".to_string());
fs::write(&path, template.to_string()).unwrap();
let template = read_template_or_blank(make_blank, &path).unwrap();
assert_eq!(but_last_line(&template.to_string()), "<div><br/><img/></div>");
}