From c4e21835d6f6431dc67857ce0fa35c5062c477f6 Mon Sep 17 00:00:00 2001 From: John-John Tedro Date: Mon, 8 May 2023 10:26:51 +0200 Subject: [PATCH] Implement search for docs --- crates/rune/src/doc/html.rs | 331 ++++++++++++--------- crates/rune/src/doc/html/enum_.rs | 4 +- crates/rune/src/doc/html/type_.rs | 31 +- crates/rune/src/doc/static/layout.html.hbs | 21 +- crates/rune/src/doc/static/runedoc.css | 71 ++++- crates/rune/src/doc/static/search.js | 281 +++++++++++++++++ 6 files changed, 586 insertions(+), 153 deletions(-) create mode 100644 crates/rune/src/doc/static/search.js diff --git a/crates/rune/src/doc/html.rs b/crates/rune/src/doc/html.rs index 1d55d7d2b..9a7697990 100644 --- a/crates/rune/src/doc/html.rs +++ b/crates/rune/src/doc/html.rs @@ -2,7 +2,7 @@ mod enum_; mod type_; mod markdown; -use std::fmt; +use std::fmt::{self, Write}; use std::fs; use std::io; use std::path::Path; @@ -40,10 +40,157 @@ const THEME: &str = "base16-eighties.dark"; #[folder = "src/doc/static"] struct Assets; +/// Write html documentation to the given path. +pub fn write_html( + name: &str, + root: &Path, + context: &crate::Context, + visitors: &[Visitor], +) -> Result<()> { + let context = Context::new(context, visitors); + + let templating = templating::Templating::new([ + ("layout", asset_str("layout.html.hbs")?), + ])?; + + let syntax_set = SyntaxSet::load_defaults_newlines(); + let theme_set = ThemeSet::load_defaults(); + + let mut fonts = Vec::new(); + let mut css = Vec::new(); + let mut js = Vec::new(); + + for file in Assets::iter() { + let path = RelativePath::new(file.as_ref()); + + match (path.file_name(), path.extension()) { + (Some(name), Some("woff2")) => { + let file = Assets::get(file.as_ref()).context("missing asset")?; + let path = copy_file(name, root, file)?; + fonts.push(path.to_owned()); + } + (Some(name), Some("css")) => { + let file = Assets::get(file.as_ref()).context("missing asset")?; + let path = copy_file(name, root, file)?; + css.push(path.to_owned()); + } + (Some(name), Some("js")) => { + let file = Assets::get(file.as_ref()).context("missing asset")?; + let path = copy_file(name, root, file)?; + js.push(path.to_owned()); + } + _ => {} + } + } + + let syntax_css = RelativePath::new("syntax.css"); + let theme = theme_set.themes.get(THEME).context("missing theme")?; + let syntax_css_content = html::css_for_theme_with_class_style(theme, html::ClassStyle::Spaced)?; + tracing::info!("writing: {}", syntax_css); + fs::write(syntax_css.to_path(root), syntax_css_content) + .with_context(|| syntax_css.to_owned())?; + css.push(syntax_css.to_owned()); + + // Collect an ordered set of modules, so we have a baseline of what to render when. + let mut initial = BTreeSet::new(); + + for module in context.iter_modules() { + let hash = Hash::type_hash(&module); + initial.insert(Build::Module(Cow::Owned(module), hash)); + } + + let search_index = RelativePath::new("index.js"); + + let mut cx = Ctxt { + root, + path: RelativePathBuf::new(), + item: ItemBuf::new(), + item_kind: ItemKind::Module, + items: Vec::new(), + name, + context: &context, + search_index: Some(search_index), + fonts: &fonts, + css: &css, + js: &js, + index_template: compile(&templating, "index.html.hbs")?, + module_template: compile(&templating, "module.html.hbs")?, + type_template: compile(&templating, "type.html.hbs")?, + macro_template: compile(&templating, "macro.html.hbs")?, + function_template: compile(&templating, "function.html.hbs")?, + enum_template: compile(&templating, "enum.html.hbs")?, + syntax_set, + }; + + let mut queue = initial.into_iter().collect::>(); + + let mut modules = Vec::new(); + + while let Some(build) = queue.pop_front() { + match build { + Build::Type(item, hash) => { + cx.set_path(item, ItemKind::Type)?; + let items = self::type_::build(&cx, "Type", "type", hash)?; + cx.items.extend(items); + } + Build::Struct(item, hash) => { + cx.set_path(item, ItemKind::Struct)?; + let items = self::type_::build(&cx, "Struct", "struct", hash)?; + cx.items.extend(items); + } + Build::Enum(item, hash) => { + cx.set_path(item, ItemKind::Enum)?; + self::enum_::build(&cx, hash)?; + } + Build::Macro(item) => { + cx.set_path(item, ItemKind::Macro)?; + build_macro(&cx)?; + } + Build::Function(item) => { + cx.set_path(item, ItemKind::Function)?; + build_function(&cx)?; + } + Build::Module(item, hash) => { + cx.set_path(item.as_ref(), ItemKind::Module)?; + module(&cx, hash, &mut queue)?; + modules.push((item, cx.path.clone())); + } + } + } + + cx.path = RelativePath::new("index.html").to_owned(); + index(&cx, &modules)?; + + cx.path = search_index.to_owned(); + cx.write_file(|cx| { + let mut s = String::new(); + write!(s, "window.INDEX = [")?; + + let mut it = cx.items.iter(); + + while let Some((path, item, kind)) = it.next() { + write!(s, "[\"{path}\", \"{item}\", \"{kind}\"]")?; + + if it.clone().next().is_some() { + write!(s, ",")?; + } + } + + write!(s, "];")?; + writeln!(s)?; + Ok(s) + })?; + + Ok(()) +} + #[derive(Serialize)] -struct Shared { +struct Shared<'a> { + path: Option<&'a RelativePath>, + search_index: Option, fonts: Vec, css: Vec, + js: Vec, } #[derive(Debug, Clone, Copy)] @@ -54,6 +201,7 @@ enum ItemKind { Module, Macro, Function, + Method, } impl fmt::Display for ItemKind { @@ -65,19 +213,24 @@ impl fmt::Display for ItemKind { ItemKind::Module => "module".fmt(f), ItemKind::Macro => "macro".fmt(f), ItemKind::Function => "function".fmt(f), + ItemKind::Method => "method".fmt(f), } } } pub(crate) struct Ctxt<'a> { root: &'a Path, + path: RelativePathBuf, item: ItemBuf, item_kind: ItemKind, - path: RelativePathBuf, + /// A collection of all items visited. + items: Vec<(RelativePathBuf, ItemBuf, ItemKind)>, name: &'a str, context: &'a Context<'a>, + search_index: Option<&'a RelativePath>, fonts: &'a [RelativePathBuf], css: &'a [RelativePathBuf], + js: &'a [RelativePathBuf], index_template: templating::Template, module_template: templating::Template, type_template: templating::Template, @@ -88,23 +241,28 @@ pub(crate) struct Ctxt<'a> { } impl Ctxt<'_> { - fn set_path(&mut self, item: &Item, kind: ItemKind) { + fn set_path(&mut self, item: &Item, kind: ItemKind) -> Result<()> { self.path = RelativePathBuf::new(); - build_item_path(self.name, item, kind, &mut self.path); + build_item_path(self.name, item, kind, &mut self.path)?; self.item = item.to_owned(); self.item_kind = kind; + self.items.push((self.path.clone(), self.item.clone(), self.item_kind)); + Ok(()) } fn dir(&self) -> &RelativePath { self.path.parent().unwrap_or(RelativePath::new("")) } - fn shared(&self) -> Shared { + fn shared(&self) -> Shared<'_> { let dir = self.dir(); Shared { + path: self.path.parent(), + search_index: self.search_index.map(|p| dir.relative(p)), fonts: self.fonts.iter().map(|f| dir.relative(f)).collect(), css: self.css.iter().map(|f| dir.relative(f)).collect(), + js: self.js.iter().map(|f| dir.relative(f)).collect(), } } @@ -177,20 +335,20 @@ impl Ctxt<'_> { } #[inline] - fn item_path(&self, item: &Item, kind: ItemKind) -> RelativePathBuf { + fn item_path(&self, item: &Item, kind: ItemKind) -> Result { let mut path = RelativePathBuf::new(); - build_item_path(self.name, item, kind, &mut path); - self.dir().relative(path) + build_item_path(self.name, item, kind, &mut path)?; + Ok(self.dir().relative(path)) } /// Build banklinks for the current item. - fn module_path_html(&self, is_module: bool) -> String { + fn module_path_html(&self, is_module: bool) -> Result { let mut module = Vec::new(); let mut iter = self.item.iter(); while iter.next_back().is_some() { if let Some(name) = iter.as_item().last() { - let url = self.item_path(iter.as_item(), ItemKind::Module); + let url = self.item_path(iter.as_item(), ItemKind::Module)?; module.push(format!("{name}")); } } @@ -203,7 +361,7 @@ impl Ctxt<'_> { } } - module.join("::") + Ok(module.join("::")) } /// Convert a hash into a link. @@ -220,15 +378,15 @@ impl Ctxt<'_> { match &meta.kind { Kind::Type => { - let path = self.item_path(meta.item, ItemKind::Type); + let path = self.item_path(meta.item, ItemKind::Type)?; format!("{name}") } Kind::Struct => { - let path = self.item_path(meta.item, ItemKind::Struct); + let path = self.item_path(meta.item, ItemKind::Struct)?; format!("{name}") } Kind::Enum => { - let path = self.item_path(meta.item, ItemKind::Enum); + let path = self.item_path(meta.item, ItemKind::Enum)?; format!("{name}") } kind => format!("{kind:?}"), @@ -407,7 +565,7 @@ impl Ctxt<'_> { return None; }; - let path = self.item_path(&item, item_path); + let path = self.item_path(&item, item_path).ok()?; let title = format!("{item_path} {link}"); Some((path, title)) } @@ -441,116 +599,6 @@ fn compile(templating: &templating::Templating, path: &str) -> Result Result<()> { - let context = Context::new(context, visitors); - - let templating = templating::Templating::new([ - ("layout", asset_str("layout.html.hbs")?), - ])?; - - let syntax_set = SyntaxSet::load_defaults_newlines(); - let theme_set = ThemeSet::load_defaults(); - - let mut fonts = Vec::new(); - let mut css = Vec::new(); - - for file in Assets::iter() { - let path = RelativePath::new(file.as_ref()); - - match (path.file_name(), path.extension()) { - (Some(name), Some("woff2")) => { - let file = Assets::get(file.as_ref()).context("missing font")?; - let path = copy_file(name, root, file)?; - fonts.push(path.to_owned()); - } - (Some(name), Some("css")) => { - let file = Assets::get(file.as_ref()).context("missing font")?; - let path = copy_file(name, root, file)?; - css.push(path.to_owned()); - } - _ => {} - } - } - - let syntax_css = RelativePath::new("syntax.css"); - let theme = theme_set.themes.get(THEME).context("missing theme")?; - let syntax_css_content = html::css_for_theme_with_class_style(theme, html::ClassStyle::Spaced)?; - tracing::info!("writing: {}", syntax_css); - fs::write(syntax_css.to_path(root), syntax_css_content) - .with_context(|| syntax_css.to_owned())?; - css.push(syntax_css.to_owned()); - - // Collect an ordered set of modules, so we have a baseline of what to render when. - let mut initial = BTreeSet::new(); - - for module in context.iter_modules() { - let hash = Hash::type_hash(&module); - initial.insert(Build::Module(Cow::Owned(module), hash)); - } - - let mut cx = Ctxt { - root, - item: ItemBuf::new(), - item_kind: ItemKind::Module, - path: RelativePathBuf::new(), - name, - context: &context, - fonts: &fonts, - css: &css, - index_template: compile(&templating, "index.html.hbs")?, - module_template: compile(&templating, "module.html.hbs")?, - type_template: compile(&templating, "type.html.hbs")?, - macro_template: compile(&templating, "macro.html.hbs")?, - function_template: compile(&templating, "function.html.hbs")?, - enum_template: compile(&templating, "enum.html.hbs")?, - syntax_set, - }; - - let mut queue = initial.into_iter().collect::>(); - - let mut modules = Vec::new(); - - while let Some(build) = queue.pop_front() { - match build { - Build::Type(item, hash) => { - cx.set_path(item, ItemKind::Type); - self::type_::build(&cx, "Type", "type", hash)?; - } - Build::Struct(item, hash) => { - cx.set_path(item, ItemKind::Struct); - self::type_::build(&cx, "Struct", "struct", hash)?; - } - Build::Enum(item, hash) => { - cx.set_path(item, ItemKind::Enum); - self::enum_::build(&cx, hash)?; - } - Build::Macro(item) => { - cx.set_path(item, ItemKind::Macro); - build_macro(&cx)?; - } - Build::Function(item) => { - cx.set_path(item, ItemKind::Function); - build_function(&cx)?; - } - Build::Module(item, hash) => { - cx.set_path(item.as_ref(), ItemKind::Module); - module(&cx, hash, &mut queue)?; - modules.push((item, cx.path.clone())); - } - } - } - - cx.path = RelativePath::new("index.html").to_owned(); - index(&cx, &modules)?; - Ok(()) -} - /// Copy an embedded file. fn copy_file<'a>( name: &'a str, @@ -570,7 +618,7 @@ fn index(cx: &Ctxt<'_>, mods: &[(Cow<'_, Item>, RelativePathBuf)]) -> Result<()> #[derive(Serialize)] struct Params<'a> { #[serde(flatten)] - shared: Shared, + shared: Shared<'a>, modules: &'a [Module<'a>], } @@ -613,7 +661,7 @@ fn module<'a>(cx: &Ctxt<'a>, hash: Hash, queue: &mut VecDeque>) -> Res #[derive(Serialize)] struct Params<'a> { #[serde(flatten)] - shared: Shared, + shared: Shared<'a>, #[serde(serialize_with = "serialize_item")] item: &'a Item, module: String, @@ -703,7 +751,7 @@ fn module<'a>(cx: &Ctxt<'a>, hash: Hash, queue: &mut VecDeque>) -> Res match meta.kind { Kind::Type { .. } => { queue.push_front(Build::Type(meta.item, meta.hash)); - let path = cx.item_path(&item, ItemKind::Type); + let path = cx.item_path(&item, ItemKind::Type)?; types.push(Type { item: item.clone(), path, @@ -713,7 +761,7 @@ fn module<'a>(cx: &Ctxt<'a>, hash: Hash, queue: &mut VecDeque>) -> Res } Kind::Struct { .. } => { queue.push_front(Build::Struct(meta.item, meta.hash)); - let path = cx.item_path(&item, ItemKind::Struct); + let path = cx.item_path(&item, ItemKind::Struct)?; structs.push(Struct { item: item.clone(), path, @@ -723,7 +771,7 @@ fn module<'a>(cx: &Ctxt<'a>, hash: Hash, queue: &mut VecDeque>) -> Res } Kind::Enum { .. } => { queue.push_front(Build::Enum(meta.item, meta.hash)); - let path = cx.item_path(&item, ItemKind::Enum); + let path = cx.item_path(&item, ItemKind::Enum)?; enums.push(Enum { item: item.clone(), path, @@ -735,7 +783,7 @@ fn module<'a>(cx: &Ctxt<'a>, hash: Hash, queue: &mut VecDeque>) -> Res queue.push_front(Build::Macro(meta.item)); macros.push(Macro { - path: cx.item_path(meta.item, ItemKind::Macro), + path: cx.item_path(meta.item, ItemKind::Macro)?, item: meta.item, name, doc: cx.render_docs(meta.docs.get(..1).unwrap_or_default())?, @@ -750,7 +798,7 @@ fn module<'a>(cx: &Ctxt<'a>, hash: Hash, queue: &mut VecDeque>) -> Res functions.push(Function { is_async: f.is_async, - path: cx.item_path(&item, ItemKind::Function), + path: cx.item_path(&item, ItemKind::Function)?, item: item.clone(), name, args: cx.args_to_string(f.args, f.signature, f.argument_types)?, @@ -764,7 +812,7 @@ fn module<'a>(cx: &Ctxt<'a>, hash: Hash, queue: &mut VecDeque>) -> Res } queue.push_front(Build::Module(Cow::Borrowed(meta.item), meta.hash)); - let path = cx.item_path(meta.item, ItemKind::Module); + let path = cx.item_path(meta.item, ItemKind::Module)?; let name = meta.item.last().context("missing name of module")?; modules.push(Module { item: meta.item, name, path }) } @@ -779,7 +827,7 @@ fn module<'a>(cx: &Ctxt<'a>, hash: Hash, queue: &mut VecDeque>) -> Res cx.module_template.render(&Params { shared: cx.shared(), item: &cx.item, - module: cx.module_path_html(true), + module: cx.module_path_html(true)?, doc: cx.render_docs(doc)?, types, structs, @@ -797,7 +845,7 @@ fn build_macro(cx: &Ctxt<'_>) -> Result<()> { #[derive(Serialize)] struct Params<'a> { #[serde(flatten)] - shared: Shared, + shared: Shared<'a>, module: String, #[serde(serialize_with = "serialize_item")] item: &'a Item, @@ -819,7 +867,7 @@ fn build_macro(cx: &Ctxt<'_>) -> Result<()> { cx.write_file(|cx| { cx.macro_template.render(&Params { shared: cx.shared(), - module: cx.module_path_html(false), + module: cx.module_path_html(false)?, item: &cx.item, name, doc, @@ -833,7 +881,7 @@ fn build_function(cx: &Ctxt<'_>) -> Result<()> { #[derive(Serialize)] struct Params<'a> { #[serde(flatten)] - shared: Shared, + shared: Shared<'a>, module: String, #[serde(serialize_with = "serialize_item")] item: &'a Item, @@ -873,7 +921,7 @@ fn build_function(cx: &Ctxt<'_>) -> Result<()> { cx.write_file(|cx| { cx.function_template.render(&Params { shared: cx.shared(), - module: cx.module_path_html(false), + module: cx.module_path_html(false)?, item: &cx.item, name, args: cx.args_to_string(args, signature, argument_types)?, @@ -919,7 +967,7 @@ fn ensure_parent_dir(path: &Path) -> Result<()> { } /// Helper for building an item path. -fn build_item_path(name: &str, item: &Item, kind: ItemKind, path: &mut RelativePathBuf) { +fn build_item_path(name: &str, item: &Item, kind: ItemKind, path: &mut RelativePathBuf) -> Result<()> { if item.is_empty() { path.push(name); } else { @@ -941,7 +989,10 @@ fn build_item_path(name: &str, item: &Item, kind: ItemKind, path: &mut RelativeP ItemKind::Module => "module.html", ItemKind::Macro => "macro.html", ItemKind::Function => "fn.html", + ItemKind::Method => bail!("Can't build toplevel path out of methods"), }); + + Ok(()) } /// Render documentation. diff --git a/crates/rune/src/doc/html/enum_.rs b/crates/rune/src/doc/html/enum_.rs index c08e22054..0dd3f0cd2 100644 --- a/crates/rune/src/doc/html/enum_.rs +++ b/crates/rune/src/doc/html/enum_.rs @@ -14,7 +14,7 @@ pub(super) fn build(cx: &Ctxt<'_>, hash: Hash) -> Result<()> { #[derive(Serialize)] struct Params<'a> { #[serde(flatten)] - shared: super::Shared, + shared: super::Shared<'a>, module: String, #[serde(serialize_with = "super::serialize_component_ref")] name: ComponentRef<'a>, @@ -24,7 +24,7 @@ pub(super) fn build(cx: &Ctxt<'_>, hash: Hash) -> Result<()> { protocols: Vec>, } - let module = cx.module_path_html(false); + let module = cx.module_path_html(false)?; let name = cx.item.last().context("missing module name")?; let (protocols, methods) = super::type_::build_assoc_fns(cx, hash)?; diff --git a/crates/rune/src/doc/html/type_.rs b/crates/rune/src/doc/html/type_.rs index f42ec72c6..512a43b9c 100644 --- a/crates/rune/src/doc/html/type_.rs +++ b/crates/rune/src/doc/html/type_.rs @@ -1,13 +1,13 @@ use crate::no_std::prelude::*; use anyhow::{Context, Result}; +use relative_path::RelativePathBuf; use serde::Serialize; -use crate::compile::{ComponentRef, Item}; +use crate::compile::{ComponentRef, Item, ItemBuf}; use crate::doc::context::{Assoc, AssocFnKind, Signature}; use crate::hash::Hash; - -use super::Ctxt; +use crate::doc::html::{Ctxt, ItemKind}; #[derive(Serialize)] pub(super) struct Protocol<'a> { @@ -111,7 +111,7 @@ pub(super) fn build_assoc_fns<'a>( #[derive(Serialize)] struct Params<'a> { #[serde(flatten)] - shared: super::Shared, + shared: super::Shared<'a>, what: &'a str, what_class: &'a str, module: String, @@ -125,12 +125,27 @@ struct Params<'a> { /// Build an unknown type. #[tracing::instrument(skip_all)] -pub(super) fn build(cx: &Ctxt<'_>, what: &str, what_class: &str, hash: Hash) -> Result<()> { - let module = cx.module_path_html(false); +pub(super) fn build(cx: &Ctxt<'_>, what: &str, what_class: &str, hash: Hash) -> Result> { + let module = cx.module_path_html(false)?; let name = cx.item.last().context("missing module name")?; let (protocols, methods) = build_assoc_fns(cx, hash)?; + let mut items = Vec::new(); + + for m in &methods { + let mut path = cx.path.clone(); + let mut item = cx.item.clone(); + + let Some(name) = path.file_name() else { + continue; + }; + + item.push(m.name); + path.set_file_name(format!("{name}#method.{}", m.name)); + items.push((path, item, ItemKind::Method)); + } + cx.write_file(|cx| { cx.type_template.render(&Params { shared: cx.shared(), @@ -142,5 +157,7 @@ pub(super) fn build(cx: &Ctxt<'_>, what: &str, what_class: &str, hash: Hash) -> methods, protocols, }) - }) + })?; + + Ok(items) } diff --git a/crates/rune/src/doc/static/layout.html.hbs b/crates/rune/src/doc/static/layout.html.hbs index 11a4e4851..aca0ddce5 100644 --- a/crates/rune/src/doc/static/layout.html.hbs +++ b/crates/rune/src/doc/static/layout.html.hbs @@ -1,8 +1,23 @@ - + {{#each fonts}}{{/each}} {{#each css}}{{/each}} - +{{#each js}}{{/each}} +{{#if search_index}}{{/if}} + +
- {{> @partial-block}} + + +
+ {{> @partial-block}} +
diff --git a/crates/rune/src/doc/static/runedoc.css b/crates/rune/src/doc/static/runedoc.css index 50c6ed4ef..7a944662e 100644 --- a/crates/rune/src/doc/static/runedoc.css +++ b/crates/rune/src/doc/static/runedoc.css @@ -6,19 +6,80 @@ --headings-border-bottom-color: #d2d2d2; --type-link-color: #2dbfb8; --fn-link-color: #2bab63; + --method-link-color: #2bab63; --macro-link-color: #09bd00; --mod-link-color: #d2991d; + --search-result-border-color: #aaa3; + --border-color: #e0e0e0; + --button-background-color: #f0f0f0; + --search-color: #111; + --search-input-focused-border-color: #008dfd; + --search-result-link-focus-background-color: #616161; +} + +* { + box-sizing: border-box; +} + +#search-input, .search-result, h1, h2, h3, h4, h5, h6 { + font-family: "Fira Sans",Arial,NanumBarunGothic,sans-serif; } #container { max-width: 960px; } +.hidden { + display: none; +} + +#search { + display: none; +} + +#search.visible { + display: block !important; +} + +#search-form { + display: flex; +} + +#search-input { + outline: none; + border: 1px solid var(--border-color); + border-radius: 2px; + padding: 8px; + font-size: 1rem; + flex-grow: 1; + background-color: var(--button-background-color); + color: var(--search-color); +} + +#search-input:focus { + border-color: var(--search-input-focused-border-color); +} + +.search-result { + display: flex; + margin-left: 2px; + margin-right: 2px; + border-bottom: 1px solid var(--search-result-border-color); + gap: 1em; +} + +.search-result a { + color: var(--text-color); +} + +.search-result:hover { + background-color: var(--search-result-link-focus-background-color); +} + body { color: var(--text-color); background-color: var(--background-color); font: 1rem/1.5 "Source Serif 4",NanumBarunGothic,serif; - font-family: "Fira Sans",Arial,NanumBarunGothic,sans-serif; margin: 20px; } @@ -89,10 +150,18 @@ a { color: var(--type-link-color); } +.enum { + color: var(--type-link-color); +} + .fn { color: var(--fn-link-color); } +.method { + color: var(--method-link-color); +} + .macro { color: var(--macro-link-color); } diff --git a/crates/rune/src/doc/static/search.js b/crates/rune/src/doc/static/search.js new file mode 100644 index 000000000..ccd5bceeb --- /dev/null +++ b/crates/rune/src/doc/static/search.js @@ -0,0 +1,281 @@ +(function(w) { + const $doc = w.document; + + let fromPath = null; + + let makePath = (path) => { + if (!fromPath) { + return path; + } + + let newPath = []; + let i = 0; + let from = fromPath.split('/'); + let to = path.split('/'); + + // strip common prefix. + while (true) { + if (from[i] === undefined || to[i] === undefined) { + break; + } + + if (from[i] !== to[i]) { + break; + } + + i += 1; + } + + for (let _ of from.slice(i)) { + newPath.push(".."); + } + + for (let p of to.slice(i)) { + newPath.push(p); + } + + return newPath.join('/'); + }; + + let getQueryVariable = (variable) => { + let query = w.location.search.substring(1); + let vars = query.split('&'); + + for (let i = 0; i < vars.length; i++) { + let pair = vars[i].split('='); + + if (decodeURIComponent(pair[0]) == variable) { + return decodeURIComponent(pair[1]); + } + } + + return null; + } + + let kindToClass = (kind) => { + switch (kind) { + case "function": + return "fn"; + default: + return kind; + } + }; + + let score = (q, item) => { + let s = 1.0; + let any = true; + let itemParts = item.split(":").filter((p) => p !== "").map((p) => p.toLowerCase()); + + for (let qp of q.toLowerCase().split(":")) { + if (qp === "") { + continue; + } + + let local = false; + let lastPart = false; + + for (let part of itemParts) { + lastPart = false; + + if (part === qp) { + local = true; + lastPart = true; + s *= 2.0; + } else if (part.startsWith(qp)) { + local = true; + lastPart = true; + s *= 1.5; + } + } + + if (lastPart) { + s *= 2.0; + } + + if (!local) { + any = false; + } + } + + if (any) { + return s; + } + + return null; + } + + let makeResult = (child, path, item, kind) => { + let linkNode = null; + + if (child.firstChild) { + linkNode = child.firstChild; + } else { + linkNode = $doc.createElement("a"); + child.appendChild(linkNode); + } + + linkNode.innerHTML = ''; + + linkNode.appendChild($doc.createTextNode(kind)); + linkNode.appendChild($doc.createTextNode(" ")); + + let parts = item.split("::"); + + if (parts.length !== 0) { + let last = parts[parts.length - 1]; + + for (let i = 0; i + 1 < parts.length; i++) { + if (parts[i] !== "") { + linkNode.appendChild($doc.createTextNode(parts[i])); + linkNode.appendChild($doc.createTextNode("::")); + } + } + + let span = $doc.createElement("span"); + span.className = kindToClass(kind); + span.appendChild($doc.createTextNode(last)); + linkNode.appendChild(span); + } + + linkNode.href = makePath(path); + } + + let removeClass = (els, className) => { + for (let el of els) { + if (el.classList.contains(className)) { + el.classList.remove(className); + } + } + }; + + let addClass = (els, className) => { + for (let el of els) { + if (!el.classList.contains(className)) { + el.classList.add(className); + } + } + }; + + let baseUrl = () => { + return w.location.href.split("?")[0].split("#")[0]; + }; + + let makeUrl = (q) => { + if (q !== '') { + q = encodeURIComponent(q); + return baseUrl() + `?search=${q}`; + } else { + return baseUrl(); + } + }; + + w.addEventListener("load", () => { + let body = $doc.querySelector("body[data-path]"); + + if (!!body) { + fromPath = body.getAttribute("data-path"); + } + + let search = $doc.querySelector("#search"); + let content = $doc.querySelector("#content"); + + if (!search || !content || !w.INDEX) { + return; + } + + let input = search.querySelector("#search-input"); + let searchResults = search.querySelector("#search-results"); + let searchTitle = search.querySelector("#search-title"); + + if (!input || !searchResults || !searchTitle) { + return; + } + + let processQuery = (q) => { + w.history.replaceState('', '', makeUrl(q)); + + if (q === '') { + removeClass([content], "hidden"); + addClass([searchResults, searchTitle], "hidden"); + return; + } + + addClass([content], "hidden"); + removeClass([searchResults, searchTitle], "hidden"); + + let results = []; + + for (let row of w.INDEX) { + let [path, item, kind] = row; + let s = score(q, item); + + if (s !== null) { + results.push([s, path, item, kind]); + } + } + + results.sort((a, b) => b[0] - a[0]); + + let i = 0; + + // Make results out of existing child nodes to avoid as much reflow + // as possible. + for (let child of searchResults.children) { + if (i >= results.length) { + break; + } + + let [_, path, item, kind] = results[i]; + makeResult(child, path, item, kind); + i += 1; + } + + while (i < results.length) { + const child = $doc.createElement("div"); + child.className = "search-result"; + let [_, path, item, kind] = results[i]; + makeResult(child, path, item, kind); + searchResults.appendChild(child); + i += 1; + } + + if (searchResults.children.length !== 0) { + for (let n = searchResults.children.length - 1; i <= n; n--) { + searchResults.removeChild(searchResults.children[n]); + } + } + }; + + if (!search.classList.contains("visible")) { + search.classList.add("visible"); + } + + let q = getQueryVariable("search"); + + if (q !== null) { + processQuery(q); + input.value = q; + } + + input.addEventListener("input", (e) => { + let q = e.target.value; + processQuery(q); + }); + + let oldAttribute = null; + + input.addEventListener("blur", (e) => { + if (oldAttribute !== null) { + input.setAttribute("placeholder", oldAttribute); + oldAttribute = null; + } + }); + + input.addEventListener("focus", (e) => { + if (oldAttribute === null) { + oldAttribute = input.getAttribute("placeholder"); + input.setAttribute("placeholder", "Type your search..."); + } + }); + }); +})(window);