Search bar for forc-doc (#5269)

## Description

Closes https://github.com/FuelLabs/sway/issues/3480

A simple search bar for forc-doc. It uses a case-insensitive search of
item names, across the library and all of its dependencies. Names in the
search results are highlighted similar to docs.rs.

![Nov-14-2023
22-21-07](0a8f5bea-eace-405c-a26e-e8c17b9756c1)

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [ ] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [ ] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.

---------

Co-authored-by: Chris O'Brien <obrchr96@gmail.com>
Co-authored-by: Joshua Batty <joshpbatty@gmail.com>
This commit is contained in:
Sophie Dankel 2023-11-15 10:47:08 -08:00 committed by GitHub
parent 6bf7d4f6fb
commit d07a84bbd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 438 additions and 118 deletions

View file

@ -13,14 +13,20 @@ anyhow = "1.0.65"
clap = { version = "4.0.18", features = ["derive"] }
colored = "2.0.0"
comrak = "0.16"
expect-test = "1.4.1"
forc-pkg = { version = "0.47.0", path = "../../forc-pkg" }
forc-util = { version = "0.47.0", path = "../../forc-util" }
horrorshow = "0.8.4"
include_dir = "0.7.3"
minifier = "0.3.0"
opener = "0.5.0"
serde = "1.0"
serde_json = "1.0"
sway-ast = { version = "0.47.0", path = "../../sway-ast" }
sway-core = { version = "0.47.0", path = "../../sway-core" }
sway-lsp = { version = "0.47.0", path = "../../sway-lsp" }
sway-types = { version = "0.47.0", path = "../../sway-types" }
swayfmt = { version = "0.47.0", path = "../../swayfmt" }
[dev-dependencies]
dir_indexer = "0.0.2"
expect-test = "1.4.1"

View file

@ -18,7 +18,7 @@ use sway_types::{BaseIdent, Spanned};
mod descriptor;
pub mod module;
#[derive(Default)]
#[derive(Default, Clone)]
pub(crate) struct Documentation(pub(crate) Vec<Document>);
impl Documentation {
/// Gather [Documentation] from the [TyProgram].
@ -214,7 +214,7 @@ impl Document {
preview_opt: self.preview_opt(),
}
}
fn preview_opt(&self) -> Option<String> {
pub(crate) fn preview_opt(&self) -> Option<String> {
create_preview(self.raw_attributes.clone())
}
}

View file

@ -10,7 +10,7 @@ pub(crate) type ModulePrefixes = Vec<String>;
/// Information about a Sway module.
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub(crate) struct ModuleInfo {
/// The preceeding module names, used in navigating between modules.
/// The preceding module names, used in navigating between modules.
pub(crate) module_prefixes: ModulePrefixes,
/// Doc attributes of a module.
/// Renders into the module level docstrings.
@ -121,6 +121,7 @@ impl ModuleInfo {
.map(|file_path_str| file_path_str.to_string())
.ok_or_else(|| anyhow::anyhow!("There will always be at least the item name"))
}
/// Compares the current `module_info` to the next `module_info` to determine how many directories to go back to make
/// the next file path valid, and returns that path as a `String`.
///
@ -169,6 +170,22 @@ impl ModuleInfo {
Ok(new_path)
}
}
/// Returns the relative path to the root of the project.
///
/// Example:
/// ```
/// current_location = "project_root/module/submodule1/submodule2/struct.Name.html"
/// result = "../.."
/// ```
/// In this case the first module to match is "module", so we have no need to go back further than that.
pub(crate) fn path_to_root(&self) -> String {
(0..self.module_prefixes.len())
.map(|_| "..")
.collect::<Vec<_>>()
.join("/")
}
/// Create a path `&str` for navigation from the `module.depth()` & `file_name`.
///
/// This is only used for shorthand path syntax, e.g `../../file_name.html`.

View file

@ -1,6 +1,7 @@
use crate::{
doc::Documentation,
render::{constant::INDEX_FILENAME, RenderedDocumentation},
search::write_search_index,
};
use anyhow::{bail, Result};
use clap::Parser;
@ -20,6 +21,7 @@ use sway_core::{language::ty::TyProgram, BuildTarget, Engines};
mod cli;
mod doc;
mod render;
mod search;
mod tests;
pub(crate) const ASSETS_DIR_NAME: &str = "static.files";
@ -99,7 +101,7 @@ fn build_docs(
program_info: ProgramInfo,
doc_path: &Path,
build_instructions: &Command,
) -> Result<()> {
) -> Result<Documentation> {
let Command {
document_private_items,
no_deps,
@ -134,7 +136,7 @@ fn build_docs(
.map(|ver| format!("Forc v{}.{}.{}", ver.major, ver.minor, ver.patch));
// render docs to HTML
let rendered_docs = RenderedDocumentation::from_raw_docs(
raw_docs,
raw_docs.clone(),
RenderPlan::new(no_deps, document_private_items, engines),
root_attributes,
ty_program.kind,
@ -145,7 +147,7 @@ fn build_docs(
write_content(rendered_docs, doc_path)?;
println!(" {}", "Finished".bold().yellow());
Ok(())
Ok(raw_docs)
}
fn write_content(rendered_docs: RenderedDocumentation, doc_path: &Path) -> Result<()> {
@ -218,10 +220,11 @@ pub fn compile_html(
&engines,
)?;
if !build_instructions.no_deps {
let raw_docs = if !build_instructions.no_deps {
let order = plan.compilation_order();
let graph = plan.graph();
let manifest_map = plan.manifest_map();
let mut raw_docs = Documentation(Vec::new());
for (node, (compile_result, _handler)) in order.iter().zip(compile_results) {
let id = &graph[*node].id();
@ -242,9 +245,12 @@ pub fn compile_html(
pkg_manifest: pkg_manifest_file,
};
build_docs(program_info, &doc_path, build_instructions)?;
raw_docs
.0
.extend(build_docs(program_info, &doc_path, build_instructions)?.0);
}
}
raw_docs
} else {
let ty_program = match compile_results
.pop()
@ -263,8 +269,10 @@ pub fn compile_html(
manifest: &manifest,
pkg_manifest,
};
build_docs(program_info, &doc_path, build_instructions)?;
}
build_docs(program_info, &doc_path, build_instructions)?
};
write_search_index(&doc_path, raw_docs)?;
Ok((doc_path, pkg_manifest.to_owned()))
}

View file

@ -1,7 +1,10 @@
//! Handles creation of `index.html` files.
use crate::{
doc::module::ModuleInfo,
render::{constant::IDENTITY, link::DocLinks, sidebar::*, BlockTitle, DocStyle, Renderable},
render::{
constant::IDENTITY, link::DocLinks, search::generate_searchbar, sidebar::*, BlockTitle,
DocStyle, Renderable,
},
RenderPlan, ASSETS_DIR_NAME,
};
use anyhow::Result;
@ -58,13 +61,14 @@ impl Renderable for AllDocIndex {
: sidebar;
main {
div(class="width-limiter") {
// : generate_searchbar();
: generate_searchbar(self.project_name.clone());
section(id="main-content", class="content") {
h1(class="fqn") {
span(class="in-band") { : "List of all items" }
}
: doc_links;
}
section(id="search", class="search-results");
}
}
script(src=format!("../{ASSETS_DIR_NAME}/highlight.js"));
@ -165,7 +169,7 @@ impl Renderable for ModuleIndex {
: sidebar;
main {
div(class="width-limiter") {
// : generate_searchbar();
: generate_searchbar(self.module_info.clone());
section(id="main-content", class="content") {
div(class="main-heading") {
h1(class="fqn") {
@ -192,6 +196,7 @@ impl Renderable for ModuleIndex {
}
: doc_links;
}
section(id="search", class="search-results");
}
}
script(src=sway_hjs);

View file

@ -2,8 +2,8 @@
use crate::{
doc::module::ModuleInfo,
render::{
constant::IDENTITY, item::context::ItemContext, sidebar::*, title::DocBlockTitle, DocStyle,
Renderable,
constant::IDENTITY, item::context::ItemContext, search::generate_searchbar, sidebar::*,
title::DocBlockTitle, DocStyle, Renderable,
},
RenderPlan, ASSETS_DIR_NAME,
};
@ -122,7 +122,7 @@ impl Renderable for ItemBody {
// this is the main code block
main {
div(class="width-limiter") {
// : generate_searchbar();
: generate_searchbar(module_info.clone());
section(id="main-content", class="content") {
div(class="main-heading") {
h1(class="fqn") {
@ -158,6 +158,7 @@ impl Renderable for ItemBody {
: item_context.unwrap();
}
}
section(id="search", class="search-results");
}
}
script(src=sway_hjs);

View file

@ -1,37 +1,92 @@
//! Generates the searchbar.
use horrorshow::{box_html, RenderBox};
use crate::doc::module::ModuleInfo;
use horrorshow::{box_html, Raw, RenderBox};
use minifier::js::minify;
// TODO: Implement Searchbar
// - Add search functionality to search bar
// - Add help.html support
// - Add settings.html support
pub(crate) fn _generate_searchbar() -> Box<dyn RenderBox> {
pub(crate) fn generate_searchbar(module_info: ModuleInfo) -> Box<dyn RenderBox> {
let path_to_root = module_info.path_to_root();
// Since this searchbar is rendered on all pages, we need to inject the path the the root into the script.
// Therefore, we can't simply import this script from a javascript file.
let minified_script = minify(&format!(r#"
function onSearchFormSubmit(event) {{
event.preventDefault();
const searchQuery = document.getElementById("search-input").value;
const url = new URL(window.location.href);
if (searchQuery) {{
url.searchParams.set('search', searchQuery);
}} else {{
url.searchParams.delete('search');
}}
history.pushState({{ search: searchQuery }}, "", url);
window.dispatchEvent(new HashChangeEvent("hashchange"));
}}
document.addEventListener('DOMContentLoaded', () => {{
const searchbar = document.getElementById("search-input");
const searchForm = document.getElementById("search-form");
searchbar.addEventListener("keyup", function(event) {{
searchForm.dispatchEvent(new Event('submit'));
}});
searchbar.addEventListener("search", function(event) {{
searchForm.dispatchEvent(new Event('submit'));
}});
function onQueryParamsChange() {{
const searchParams = new URLSearchParams(window.location.search);
const query = searchParams.get("search");
const searchSection = document.getElementById('search');
const mainSection = document.getElementById('main-content');
const searchInput = document.getElementById('search-input');
if (query) {{
searchInput.value = query;
const results = Object.values(SEARCH_INDEX).flat().filter(item => {{
const lowerQuery = query.toLowerCase();
return item.name.toLowerCase().includes(lowerQuery);
}});
const header = `<h1>Results for ${{query}}</h1>`;
if (results.length > 0) {{
const resultList = results.map(item => {{
const formattedName = `<span class="type ${{item.type_name}}">${{item.name}}</span>`;
const name = [...item.module_info, formattedName].join("::");
const path = ["{}", ...item.module_info, item.html_filename].join("/");
const left = `<td><span>${{name}}</span></td>`;
const right = `<td><p>${{item.preview}}</p></td>`;
return `<tr onclick="window.location='${{path}}';">${{left}}${{right}}</tr>`;
}}).join('');
searchSection.innerHTML = `${{header}}<table>${{resultList}}</table>`;
}} else {{
searchSection.innerHTML = `${{header}}<p>No results found.</p>`;
}}
searchSection.setAttribute("class", "search-results");
mainSection.setAttribute("class", "content hidden");
}} else {{
searchSection.setAttribute("class", "search-results hidden");
mainSection.setAttribute("class", "content");
}}
}}
window.addEventListener('hashchange', onQueryParamsChange);
// Check for any query parameters initially
onQueryParamsChange();
}}
);"#, path_to_root)).to_string();
box_html! {
script(src=format!("{}/search.js", path_to_root), type="text/javascript");
script {
: Raw(minified_script)
}
nav(class="sub") {
form(class="search-form") {
form(id="search-form", class="search-form", onsubmit="onSearchFormSubmit(event)") {
div(class="search-container") {
span;
input(
id="search-input",
class="search-input",
name="search",
autocomplete="off",
spellcheck="false",
placeholder="Click or press S to search, ? for more options…",
placeholder="Search the docs...",
type="search"
);
div(id="help-button", title="help", tabindex="-1") {
a(href="../help.html") { : "?" }
}
div(id="settings-menu", tabindex="-1") {
a(href="../settings.html", title="settings") {
img(
width="22",
height="22",
alt="change settings",
src="../static.files/wheel.svg"
)
}
}
}
}
}

View file

@ -0,0 +1,67 @@
use crate::doc::{Document, Documentation};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::{collections::HashMap, path::Path};
const JS_SEARCH_FILE_NAME: &str = "search.js";
/// Creates the search index javascript file for the search bar.
pub(crate) fn write_search_index(doc_path: &Path, docs: Documentation) -> Result<()> {
let json_data = docs.to_json_value()?;
let module_export =
"\"object\"==typeof exports&&\"undefined\"!=typeof module&&(module.exports=SEARCH_INDEX);";
let js_data = format!("var SEARCH_INDEX={};\n{}", json_data, module_export);
Ok(fs::write(doc_path.join(JS_SEARCH_FILE_NAME), js_data)?)
}
impl Documentation {
/// Generates a mapping of program name to a vector of documentable items within the program
/// and returns the map as a `serde_json::Value`.
fn to_json_value(&self) -> Result<serde_json::Value, serde_json::Error> {
let mut map: HashMap<String, Vec<JsonSearchItem>> = HashMap::with_capacity(self.0.len());
for doc in self.0.iter() {
match map.get_mut(doc.module_info.project_name()) {
Some(items) => {
items.push(JsonSearchItem::from(doc));
}
None => {
map.insert(
doc.module_info.project_name().to_string(),
vec![JsonSearchItem::from(doc)],
);
}
}
}
serde_json::to_value(map)
}
}
/// Item information used in the `search_pool.json`.
/// The item name is what the fuzzy search will be
/// matching on, all other information will be used
/// in generating links to the item.
#[derive(Clone, Debug, Serialize, Deserialize)]
struct JsonSearchItem {
name: String,
html_filename: String,
module_info: Vec<String>,
preview: String,
type_name: String,
}
impl<'a> From<&'a Document> for JsonSearchItem {
fn from(value: &'a Document) -> Self {
Self {
name: value.item_body.item_name.to_string(),
html_filename: value.html_filename(),
module_info: value.module_info.module_prefixes.clone(),
preview: value
.preview_opt()
.unwrap_or_default()
.replace("<br>", "")
.replace("<p>", "")
.replace("</p>", ""),
type_name: value.item_body.ty_decl.friendly_type_name().to_string(),
}
}
}

View file

@ -77,19 +77,6 @@ pre,
.docblock table th {
border-color: #5c6773;
}
.search-results a:hover {
background-color: #777;
}
.search-results a:focus {
color: #000 !important;
background-color: #c6afb3;
}
.search-results a {
color: #0096cf;
}
.search-results a div.desc {
color: #c5c5c5;
}
.content .item-info::before {
color: #ccc;
}
@ -193,9 +180,8 @@ a {
.sidebar h3 a {
color: white;
}
.search-results a {
color: #0096cf;
}
body.source .example-wrap pre.sway a {
background: #333;
}
@ -427,12 +413,6 @@ kbd {
#theme-choices > button:focus {
background-color: rgba(110, 110, 110, 0.33);
}
.search-results .result-name span.alias {
color: #c5c5c5;
}
.search-results .result-name span.grey {
color: #999;
}
#source-sidebar > .title {
color: #fff;
border-bottom-color: #5c6773;

View file

@ -908,12 +908,7 @@ table,
width: 100%;
}
.search-results {
display: none;
padding-bottom: 2em;
}
.search-results.active {
display: block;
clear: both;
}
.search-results .desc > span {
white-space: nowrap;
@ -921,29 +916,44 @@ table,
overflow: hidden;
display: block;
}
.search-results > a {
display: block;
width: 100%;
margin-left: 2px;
margin-right: 2px;
border-bottom: 1px solid #aaa3;
.search-results table {
width: 100%;
white-space: nowrap;
table-layout: fixed;
}
.search-results > a > div {
display: flex;
flex-flow: row wrap;
.search-results tr {
cursor: pointer;
height:28px;
border-bottom:1px solid #28323E;
}
.search-results .result-name,
.search-results div.desc,
.search-results .result-description {
width: 50%;
.search-results tr:hover {
background-color: #28323E;
}
.search-results .result-name {
padding-right: 1em;
}
.search-results .result-name > span {
display: inline-block;
.search-results p {
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
font-weight: normal;
}
.search-results td:first-child {
padding-right: 15px;
white-space: initial;
}
.search-results td:first-child span {
font-family: monospace;
font-size: 14px;
}
.search-results .type.struct,
.search-results .type.enum {
color: #2DBFB8;
}
.search-results .type.function {
color: #2BAB63;
}
.search-results .type.trait {
color: #B78CF2;
}
.search-results .type.constant {
color: #D2991D;
}
.popover {
font-size: 1rem;

View file

@ -0,0 +1,8 @@
[[package]]
name = "core"
source = "path+from-root-09A5B00ACCB1BF9C"
[[package]]
name = "impl_traits_clone"
source = "member"
dependencies = ["core"]

View file

@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "lib.sw"
license = "Apache-2.0"
name = "impl_traits_clone"
[dependencies]
core = { path = "../../../../../../sway-lib-core" }

View file

@ -0,0 +1,29 @@
library;
use ::foo::{Foo, Baz};
use core::ops::Add;
pub struct Bar {}
impl Foo for Bar {
/// something more about foo();
fn foo() {}
}
impl Baz for Bar {}
impl Bar {
fn foo_bar() {
Self::foo()
}
}
// test dependency impls
impl Add for Bar {
fn add(self, other: Self) -> Self {
Bar {}
}
}
impl core::ops::Subtract for Bar {
fn subtract(self, other: Self) -> Self {
Bar {}
}
}

View file

@ -0,0 +1,7 @@
library;
pub trait Foo {
/// something about foo...
fn foo();
}
pub trait Baz {}

View file

@ -0,0 +1,4 @@
library;
mod foo;
mod bar;

View file

@ -1,45 +1,143 @@
#![cfg(test)]
use crate::{cli::Command, tests::expects::check};
use expect_test::expect;
use std::path::PathBuf;
use crate::{
cli::Command,
compile_html,
tests::expects::{check_file, get_doc_dir},
};
use dir_indexer::get_relative_file_paths_set;
use expect_test::{expect, Expect};
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
/// The path to the generated HTML of the type the traits are implemented on.
const IMPL_FOR: &str = "impl_traits/bar/struct.Bar.html";
const IMPL_TRAIT_FILE_PATH: &str = "src/tests/data/impl_traits";
const IMPL_FOR: &str = "bar/struct.Bar.html";
const DATA_DIR: &str = "src/tests/data";
const JS_SEARCH_FILE_PATH: &str = "search.js";
#[test]
fn impl_traits_default() {
const DOC_DIR_NAME: &str = "impl_traits_default";
fn test_impl_traits_default() {
let doc_dir_name: &str = "impl_traits_default";
let project_name = "impl_traits";
let command = Command {
manifest_path: Some(IMPL_TRAIT_FILE_PATH.into()),
doc_path: Some(DOC_DIR_NAME.into()),
manifest_path: Some(format!("{}/{}", DATA_DIR, project_name)),
doc_path: Some(doc_dir_name.into()),
..Default::default()
};
let path_to_file = PathBuf::from(IMPL_FOR);
check(
command,
path_to_file,
&expect![[r##"
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="swaydoc"><meta name="description" content="API documentation for the Sway `Bar` struct in `bar`."><meta name="keywords" content="sway, swaylang, sway-lang, Bar"><link rel="icon" href="../../static.files/sway-logo.svg"><title>Bar in bar - Sway</title><link rel="stylesheet" type="text/css" href="../../static.files/normalize.css"><link rel="stylesheet" type="text/css" href="../../static.files/swaydoc.css" id="mainThemeStyle"><link rel="stylesheet" type="text/css" href="../../static.files/ayu.css"><link rel="stylesheet" href="../../static.files/ayu.min.css"></head><body class="swaydoc struct"><nav class="sidebar"><a class="sidebar-logo" href="../../impl_traits/index.html"><div class="logo-container"><img class="sway-logo" src="../../static.files/sway-logo.svg" alt="logo"></div></a><h2 class="location">Struct Bar</h2><div class="sidebar-elems"><section><div class="block"><ul></ul></div></section></div></nav><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><h1 class="fqn"><span class="in-band">Struct <a class="mod" href="../index.html">impl_traits</a><span>::</span><a class="mod" href="index.html">bar</a><span>::</span><a class="struct" href="#">Bar</a></span></h1></div><div class="docblock item-decl"><pre class="sway struct"><code>pub struct Bar {}</code></pre></div><h2 id="trait-implementations" class="small-section-header">Trait Implementations<a href="#trait-implementations" class="anchor"></a></h2><div id="trait-implementations-list"><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Foo" class="impl has-srclink"><a href="#impl-Foo" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../foo/trait.Foo.html">Foo</a> for Bar</h3></div></summary><div class="impl-items"><details class="swaydoc-toggle method-toggle" open><summary><div id="method.foo" class="method trait-impl"><a href="#method.foo" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.foo">foo</a>()</h4></div></summary><div class="doc-block"><p>something more about foo();</p>
</div></details></div></details><div id="impl-Baz" class="impl has-srclink"><a href="#impl-Baz" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../foo/trait.Baz.html">Baz</a> for Bar</h3></div><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Add" class="impl has-srclink"><a href="#impl-Add" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../../core/ops/trait.Add.html">Add</a> for Bar</h3></div></summary><div class="impl-items"><div id="method.add" class="method trait-impl"><a href="#method.add" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.add">add</a>(self, other: Self) -&gt; Self</h4></div></div></details><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Subtract" class="impl has-srclink"><a href="#impl-Subtract" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../../core/ops/trait.Subtract.html">Subtract</a> for Bar</h3></div></summary><div class="impl-items"><div id="method.subtract" class="method trait-impl"><a href="#method.subtract" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.subtract">subtract</a>(self, other: Self) -&gt; Self</h4></div></div></details></div></section></div></main><script src="../../static.files/highlight.js"></script><script>hljs.highlightAll();</script></body></html>"##]],
let (doc_path, _) = compile_html(&command, &get_doc_dir).unwrap();
assert_index_html(
&doc_path,
project_name,
&expect![[
r##"<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="swaydoc"><meta name="description" content="API documentation for the Sway `Bar` struct in `bar`."><meta name="keywords" content="sway, swaylang, sway-lang, Bar"><link rel="icon" href="../../static.files/sway-logo.svg"><title>Bar in bar - Sway</title><link rel="stylesheet" type="text/css" href="../../static.files/normalize.css"><link rel="stylesheet" type="text/css" href="../../static.files/swaydoc.css" id="mainThemeStyle"><link rel="stylesheet" type="text/css" href="../../static.files/ayu.css"><link rel="stylesheet" href="../../static.files/ayu.min.css"></head><body class="swaydoc struct"><nav class="sidebar"><a class="sidebar-logo" href="../../impl_traits/index.html"><div class="logo-container"><img class="sway-logo" src="../../static.files/sway-logo.svg" alt="logo"></div></a><h2 class="location">Struct Bar</h2><div class="sidebar-elems"><section><div class="block"><ul></ul></div></section></div></nav><main><div class="width-limiter"><script src="../../search.js" type="text/javascript"></script><script>function onSearchFormSubmit(event){event.preventDefault();const searchQuery=document.getElementById("search-input").value;const url=new URL(window.location.href);if(searchQuery){url.searchParams.set('search',searchQuery)}else{url.searchParams.delete('search')}history.pushState({search:searchQuery},"",url);window.dispatchEvent(new HashChangeEvent("hashchange"))}document.addEventListener('DOMContentLoaded',()=>{const searchbar=document.getElementById("search-input");const searchForm=document.getElementById("search-form");searchbar.addEventListener("keyup",function(event){searchForm.dispatchEvent(new Event('submit'))});searchbar.addEventListener("search",function(event){searchForm.dispatchEvent(new Event('submit'))});function onQueryParamsChange(){const searchParams=new URLSearchParams(window.location.search);const query=searchParams.get("search");const searchSection=document.getElementById('search');const mainSection=document.getElementById('main-content');const searchInput=document.getElementById('search-input');if(query){searchInput.value=query;const results=Object.values(SEARCH_INDEX).flat().filter(item=>{const lowerQuery=query.toLowerCase();return item.name.toLowerCase().includes(lowerQuery)});const header=`<h1>Results for ${query}</h1>`;if(results.length>0){const resultList=results.map(item=>{const formattedName=`<span class="type ${item.type_name}">${item.name}</span>`;const name=[...item.module_info,formattedName].join("::");const path=["../..",...item.module_info,item.html_filename].join("/");const left=`<td><span>${name}</span></td>`;const right=`<td><p>${item.preview}</p></td>`;return`<tr onclick="window.location='${path}';">${left}${right}</tr>`}).join('');searchSection.innerHTML=`${header}<table>${resultList}</table>`}else{searchSection.innerHTML=`${header}<p>No results found.</p>`}searchSection.setAttribute("class","search-results");mainSection.setAttribute("class","content hidden")}else{searchSection.setAttribute("class","search-results hidden");mainSection.setAttribute("class","content")}}window.addEventListener('hashchange',onQueryParamsChange);onQueryParamsChange()})</script><nav class="sub"><form id="search-form" class="search-form" onsubmit="onSearchFormSubmit(event)"><div class="search-container"><input id="search-input" class="search-input" name="search" autocomplete="off" spellcheck="false" placeholder="Search the docs..." type="search"></div></form></nav><section id="main-content" class="content"><div class="main-heading"><h1 class="fqn"><span class="in-band">Struct <a class="mod" href="../index.html">impl_traits</a><span>::</span><a class="mod" href="index.html">bar</a><span>::</span><a class="struct" href="#">Bar</a></span></h1></div><div class="docblock item-decl"><pre class="sway struct"><code>pub struct Bar {}</code></pre></div><h2 id="trait-implementations" class="small-section-header">Trait Implementations<a href="#trait-implementations" class="anchor"></a></h2><div id="trait-implementations-list"><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Foo" class="impl has-srclink"><a href="#impl-Foo" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../foo/trait.Foo.html">Foo</a> for Bar</h3></div></summary><div class="impl-items"><details class="swaydoc-toggle method-toggle" open><summary><div id="method.foo" class="method trait-impl"><a href="#method.foo" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.foo">foo</a>()</h4></div></summary><div class="doc-block"><p>something more about foo();</p>
</div></details></div></details><div id="impl-Baz" class="impl has-srclink"><a href="#impl-Baz" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../foo/trait.Baz.html">Baz</a> for Bar</h3></div><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Add" class="impl has-srclink"><a href="#impl-Add" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../../core/ops/trait.Add.html">Add</a> for Bar</h3></div></summary><div class="impl-items"><div id="method.add" class="method trait-impl"><a href="#method.add" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.add">add</a>(self, other: Self) -&gt; Self</h4></div></div></details><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Subtract" class="impl has-srclink"><a href="#impl-Subtract" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../../core/ops/trait.Subtract.html">Subtract</a> for Bar</h3></div></summary><div class="impl-items"><div id="method.subtract" class="method trait-impl"><a href="#method.subtract" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.subtract">subtract</a>(self, other: Self) -&gt; Self</h4></div></div></details></div></section><section id="search" class="search-results"></section></div></main><script src="../../static.files/highlight.js"></script><script>hljs.highlightAll();</script></body></html>"##
]],
);
assert_search_js(
&doc_path,
&expect![[
r#"var SEARCH_INDEX={"core":[{"html_filename":"trait.AsRawSlice.html","module_info":["core","raw_slice"],"name":"AsRawSlice","preview":"Trait to return a type as a <code>raw_slice</code>.\n","type_name":"trait"},{"html_filename":"fn.from_str_array.html","module_info":["core","str"],"name":"from_str_array","preview":"","type_name":"function"},{"html_filename":"trait.Add.html","module_info":["core","ops"],"name":"Add","preview":"Trait for the addition of two values.\n","type_name":"trait"},{"html_filename":"trait.Subtract.html","module_info":["core","ops"],"name":"Subtract","preview":"Trait for the subtraction of two values.\n","type_name":"trait"},{"html_filename":"trait.Multiply.html","module_info":["core","ops"],"name":"Multiply","preview":"Trait for the multiplication of two values.\n","type_name":"trait"},{"html_filename":"trait.Divide.html","module_info":["core","ops"],"name":"Divide","preview":"Trait for the division of two values.\n","type_name":"trait"},{"html_filename":"trait.Mod.html","module_info":["core","ops"],"name":"Mod","preview":"Trait for the modulo of two values.\n","type_name":"trait"},{"html_filename":"trait.Not.html","module_info":["core","ops"],"name":"Not","preview":"Trait to invert a type.\n","type_name":"trait"},{"html_filename":"trait.Eq.html","module_info":["core","ops"],"name":"Eq","preview":"Trait to evaluate if two types are equal.\n","type_name":"trait"},{"html_filename":"trait.Ord.html","module_info":["core","ops"],"name":"Ord","preview":"Trait to evaluate if one value is greater or less than another of the same type.\n","type_name":"trait"},{"html_filename":"trait.BitwiseAnd.html","module_info":["core","ops"],"name":"BitwiseAnd","preview":"Trait to bitwise AND two values of the same type.\n","type_name":"trait"},{"html_filename":"trait.BitwiseOr.html","module_info":["core","ops"],"name":"BitwiseOr","preview":"Trait to bitwise OR two values of the same type.\n","type_name":"trait"},{"html_filename":"trait.BitwiseXor.html","module_info":["core","ops"],"name":"BitwiseXor","preview":"Trait to bitwise XOR two values of the same type.\n","type_name":"trait"},{"html_filename":"trait.Shift.html","module_info":["core","ops"],"name":"Shift","preview":"Trait to bit shift a value.\n","type_name":"trait"},{"html_filename":"enum.Never.html","module_info":["core","never"],"name":"Never","preview":"<code>Never</code> represents the type of computations which never resolve to any value at all.","type_name":"enum"},{"html_filename":"struct.StorageKey.html","module_info":["core","storage"],"name":"StorageKey","preview":"Describes a location in storage.\n","type_name":"struct"}],"impl_traits":[{"html_filename":"trait.Foo.html","module_info":["impl_traits","foo"],"name":"Foo","preview":"","type_name":"trait"},{"html_filename":"trait.Baz.html","module_info":["impl_traits","foo"],"name":"Baz","preview":"","type_name":"trait"},{"html_filename":"struct.Bar.html","module_info":["impl_traits","bar"],"name":"Bar","preview":"","type_name":"struct"}]};
"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=SEARCH_INDEX);"#
]],
);
assert_file_tree(
doc_dir_name,
project_name,
vec![
"core/str/fn.from_str_array.html",
"core/storage/index.html",
"core/ops/trait.Add.html",
"core/ops/index.html",
"core/index.html",
"core/ops/trait.Subtract.html",
"search.js",
"core/storage/struct.StorageKey.html",
"core/ops/trait.Mod.html",
"core/ops/trait.Shift.html",
"core/ops/trait.Not.html",
"impl_traits/foo/trait.Baz.html",
"core/never/index.html",
"core/all.html",
"core/raw_slice/trait.AsRawSlice.html",
"core/ops/trait.BitwiseAnd.html",
"core/ops/trait.Eq.html",
"core/ops/trait.BitwiseOr.html",
"core/str/index.html",
"core/raw_slice/index.html",
"impl_traits/all.html",
"impl_traits/index.html",
"core/ops/trait.BitwiseXor.html",
"core/ops/trait.Ord.html",
"core/ops/trait.Divide.html",
"impl_traits/foo/trait.Foo.html",
"impl_traits/bar/struct.Bar.html",
"impl_traits/foo/index.html",
"core/ops/trait.Multiply.html",
"impl_traits/bar/index.html",
"core/never/enum.Never.html",
],
);
}
#[test]
fn impl_traits_no_deps() {
const DOC_DIR_NAME: &str = "impl_traits_no_deps";
fn test_impl_traits_no_deps() {
let doc_dir_name: &str = "impl_traits_no_deps";
let project_name: &str = "impl_traits_clone";
let command = Command {
manifest_path: Some(IMPL_TRAIT_FILE_PATH.into()),
doc_path: Some(DOC_DIR_NAME.into()),
manifest_path: Some(format!("{}/{}", DATA_DIR, project_name)),
doc_path: Some(doc_dir_name.into()),
no_deps: true,
..Default::default()
};
let path_to_file = PathBuf::from(IMPL_FOR);
check(
command,
path_to_file,
&expect![[r##"
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="swaydoc"><meta name="description" content="API documentation for the Sway `Bar` struct in `bar`."><meta name="keywords" content="sway, swaylang, sway-lang, Bar"><link rel="icon" href="../../static.files/sway-logo.svg"><title>Bar in bar - Sway</title><link rel="stylesheet" type="text/css" href="../../static.files/normalize.css"><link rel="stylesheet" type="text/css" href="../../static.files/swaydoc.css" id="mainThemeStyle"><link rel="stylesheet" type="text/css" href="../../static.files/ayu.css"><link rel="stylesheet" href="../../static.files/ayu.min.css"></head><body class="swaydoc struct"><nav class="sidebar"><a class="sidebar-logo" href="../../impl_traits/index.html"><div class="logo-container"><img class="sway-logo" src="../../static.files/sway-logo.svg" alt="logo"></div></a><h2 class="location">Struct Bar</h2><div class="sidebar-elems"><section><div class="block"><ul></ul></div></section></div></nav><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><h1 class="fqn"><span class="in-band">Struct <a class="mod" href="../index.html">impl_traits</a><span>::</span><a class="mod" href="index.html">bar</a><span>::</span><a class="struct" href="#">Bar</a></span></h1></div><div class="docblock item-decl"><pre class="sway struct"><code>pub struct Bar {}</code></pre></div><h2 id="trait-implementations" class="small-section-header">Trait Implementations<a href="#trait-implementations" class="anchor"></a></h2><div id="trait-implementations-list"><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Foo" class="impl has-srclink"><a href="#impl-Foo" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../foo/trait.Foo.html">Foo</a> for Bar</h3></div></summary><div class="impl-items"><details class="swaydoc-toggle method-toggle" open><summary><div id="method.foo" class="method trait-impl"><a href="#method.foo" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.foo">foo</a>()</h4></div></summary><div class="doc-block"><p>something more about foo();</p>
</div></details></div></details><div id="impl-Baz" class="impl has-srclink"><a href="#impl-Baz" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../foo/trait.Baz.html">Baz</a> for Bar</h3></div><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Add" class="impl has-srclink"><a href="#impl-Add" class="anchor"></a><h3 class="code-header in-band">impl Add for Bar</h3></div></summary><div class="impl-items"><div id="method.add" class="method trait-impl"><a href="#method.add" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.add">add</a>(self, other: Self) -&gt; Self</h4></div></div></details><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Subtract" class="impl has-srclink"><a href="#impl-Subtract" class="anchor"></a><h3 class="code-header in-band">impl Subtract for Bar</h3></div></summary><div class="impl-items"><div id="method.subtract" class="method trait-impl"><a href="#method.subtract" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.subtract">subtract</a>(self, other: Self) -&gt; Self</h4></div></div></details></div></section></div></main><script src="../../static.files/highlight.js"></script><script>hljs.highlightAll();</script></body></html>"##]],
let (doc_path, _) = compile_html(&command, &get_doc_dir).unwrap();
assert_index_html(
&doc_path,
project_name,
&expect![[
r##"<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="swaydoc"><meta name="description" content="API documentation for the Sway `Bar` struct in `bar`."><meta name="keywords" content="sway, swaylang, sway-lang, Bar"><link rel="icon" href="../../static.files/sway-logo.svg"><title>Bar in bar - Sway</title><link rel="stylesheet" type="text/css" href="../../static.files/normalize.css"><link rel="stylesheet" type="text/css" href="../../static.files/swaydoc.css" id="mainThemeStyle"><link rel="stylesheet" type="text/css" href="../../static.files/ayu.css"><link rel="stylesheet" href="../../static.files/ayu.min.css"></head><body class="swaydoc struct"><nav class="sidebar"><a class="sidebar-logo" href="../../impl_traits_clone/index.html"><div class="logo-container"><img class="sway-logo" src="../../static.files/sway-logo.svg" alt="logo"></div></a><h2 class="location">Struct Bar</h2><div class="sidebar-elems"><section><div class="block"><ul></ul></div></section></div></nav><main><div class="width-limiter"><script src="../../search.js" type="text/javascript"></script><script>function onSearchFormSubmit(event){event.preventDefault();const searchQuery=document.getElementById("search-input").value;const url=new URL(window.location.href);if(searchQuery){url.searchParams.set('search',searchQuery)}else{url.searchParams.delete('search')}history.pushState({search:searchQuery},"",url);window.dispatchEvent(new HashChangeEvent("hashchange"))}document.addEventListener('DOMContentLoaded',()=>{const searchbar=document.getElementById("search-input");const searchForm=document.getElementById("search-form");searchbar.addEventListener("keyup",function(event){searchForm.dispatchEvent(new Event('submit'))});searchbar.addEventListener("search",function(event){searchForm.dispatchEvent(new Event('submit'))});function onQueryParamsChange(){const searchParams=new URLSearchParams(window.location.search);const query=searchParams.get("search");const searchSection=document.getElementById('search');const mainSection=document.getElementById('main-content');const searchInput=document.getElementById('search-input');if(query){searchInput.value=query;const results=Object.values(SEARCH_INDEX).flat().filter(item=>{const lowerQuery=query.toLowerCase();return item.name.toLowerCase().includes(lowerQuery)});const header=`<h1>Results for ${query}</h1>`;if(results.length>0){const resultList=results.map(item=>{const formattedName=`<span class="type ${item.type_name}">${item.name}</span>`;const name=[...item.module_info,formattedName].join("::");const path=["../..",...item.module_info,item.html_filename].join("/");const left=`<td><span>${name}</span></td>`;const right=`<td><p>${item.preview}</p></td>`;return`<tr onclick="window.location='${path}';">${left}${right}</tr>`}).join('');searchSection.innerHTML=`${header}<table>${resultList}</table>`}else{searchSection.innerHTML=`${header}<p>No results found.</p>`}searchSection.setAttribute("class","search-results");mainSection.setAttribute("class","content hidden")}else{searchSection.setAttribute("class","search-results hidden");mainSection.setAttribute("class","content")}}window.addEventListener('hashchange',onQueryParamsChange);onQueryParamsChange()})</script><nav class="sub"><form id="search-form" class="search-form" onsubmit="onSearchFormSubmit(event)"><div class="search-container"><input id="search-input" class="search-input" name="search" autocomplete="off" spellcheck="false" placeholder="Search the docs..." type="search"></div></form></nav><section id="main-content" class="content"><div class="main-heading"><h1 class="fqn"><span class="in-band">Struct <a class="mod" href="../index.html">impl_traits_clone</a><span>::</span><a class="mod" href="index.html">bar</a><span>::</span><a class="struct" href="#">Bar</a></span></h1></div><div class="docblock item-decl"><pre class="sway struct"><code>pub struct Bar {}</code></pre></div><h2 id="trait-implementations" class="small-section-header">Trait Implementations<a href="#trait-implementations" class="anchor"></a></h2><div id="trait-implementations-list"><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Foo" class="impl has-srclink"><a href="#impl-Foo" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../foo/trait.Foo.html">Foo</a> for Bar</h3></div></summary><div class="impl-items"><details class="swaydoc-toggle method-toggle" open><summary><div id="method.foo" class="method trait-impl"><a href="#method.foo" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.foo">foo</a>()</h4></div></summary><div class="doc-block"><p>something more about foo();</p>
</div></details></div></details><div id="impl-Baz" class="impl has-srclink"><a href="#impl-Baz" class="anchor"></a><h3 class="code-header in-band">impl <a class="trait" href="../foo/trait.Baz.html">Baz</a> for Bar</h3></div><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Add" class="impl has-srclink"><a href="#impl-Add" class="anchor"></a><h3 class="code-header in-band">impl Add for Bar</h3></div></summary><div class="impl-items"><div id="method.add" class="method trait-impl"><a href="#method.add" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.add">add</a>(self, other: Self) -&gt; Self</h4></div></div></details><details class="swaydoc-toggle implementors-toggle"><summary><div id="impl-Subtract" class="impl has-srclink"><a href="#impl-Subtract" class="anchor"></a><h3 class="code-header in-band">impl Subtract for Bar</h3></div></summary><div class="impl-items"><div id="method.subtract" class="method trait-impl"><a href="#method.subtract" class="anchor"></a><h4 class="code-header">fn <a class="fnname" href="#method.subtract">subtract</a>(self, other: Self) -&gt; Self</h4></div></div></details></div></section><section id="search" class="search-results"></section></div></main><script src="../../static.files/highlight.js"></script><script>hljs.highlightAll();</script></body></html>"##
]],
);
assert_search_js(
&doc_path,
&expect![[
r#"var SEARCH_INDEX={"impl_traits_clone":[{"html_filename":"trait.Foo.html","module_info":["impl_traits_clone","foo"],"name":"Foo","preview":"","type_name":"trait"},{"html_filename":"trait.Baz.html","module_info":["impl_traits_clone","foo"],"name":"Baz","preview":"","type_name":"trait"},{"html_filename":"struct.Bar.html","module_info":["impl_traits_clone","bar"],"name":"Bar","preview":"","type_name":"struct"}]};
"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=SEARCH_INDEX);"#
]],
);
assert_file_tree(
doc_dir_name,
project_name,
vec![
"impl_traits_clone/index.html",
"impl_traits_clone/all.html",
"impl_traits_clone/foo/trait.Foo.html",
"impl_traits_clone/bar/index.html",
"impl_traits_clone/foo/index.html",
"impl_traits_clone/foo/trait.Baz.html",
"search.js",
"impl_traits_clone/bar/struct.Bar.html",
],
);
}
fn assert_index_html(doc_path: &Path, project_name: &str, expect: &Expect) {
let path_to_file = PathBuf::from(format!("{}/{}", project_name, IMPL_FOR));
check_file(doc_path, &path_to_file, expect);
}
fn assert_search_js(doc_path: &Path, expect: &Expect) {
let path_to_file = PathBuf::from(JS_SEARCH_FILE_PATH);
check_file(doc_path, &path_to_file, expect);
}
fn assert_file_tree(doc_dir_name: &str, project_name: &str, expected_files: Vec<&str>) {
let doc_root = format!("{}/{}/out/{}", DATA_DIR, project_name, doc_dir_name).into();
let expected = expected_files
.iter()
.map(PathBuf::from)
.collect::<HashSet<PathBuf>>();
let files = get_relative_file_paths_set(doc_root);
assert_eq!(files, expected);
}

View file

@ -1,16 +1,17 @@
#![cfg(test)]
use crate::{cli::Command, compile_html};
use crate::cli::Command;
use expect_test::Expect;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
mod impl_trait;
pub(crate) fn check(command: Command, path_to_file: PathBuf, expect: &Expect) {
let (doc_path, _) = compile_html(&command, &get_doc_dir).unwrap();
let actual = std::fs::read_to_string(doc_path.join(path_to_file)).unwrap();
pub(crate) fn check_file(doc_path: &Path, path_to_file: &PathBuf, expect: &Expect) {
let path = doc_path.join(path_to_file);
let actual = std::fs::read_to_string(path.clone())
.unwrap_or_else(|_| panic!("failed to read file: {:?}", path));
expect.assert_eq(&actual)
}
fn get_doc_dir(build_instructions: &Command) -> String {
pub(crate) fn get_doc_dir(build_instructions: &Command) -> String {
build_instructions.doc_path.to_owned().unwrap()
}