diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs index 6868c7707adc8..5263cfe7b0e4f 100644 --- a/src/librustdoc/html/layout.rs +++ b/src/librustdoc/html/layout.rs @@ -33,7 +33,7 @@ pub struct Page<'a> { pub fn render( dst: &mut dyn io::Write, layout: &Layout, page: &Page, sidebar: &S, t: &T, - css_file_extension: bool, themes: &[PathBuf]) + css_file_extension: bool, themes: &[PathBuf], extra_scripts: &[&str]) -> io::Result<()> { write!(dst, @@ -149,6 +149,7 @@ pub fn render( \ \ \ + {extra_scripts}\ \ \ ", @@ -192,6 +193,11 @@ pub fn render( page.resource_suffix)) .collect::(), suffix=page.resource_suffix, + extra_scripts=extra_scripts.iter().map(|e| { + format!("", + root_path=page.root_path, + extra_script=e) + }).collect::(), ) } diff --git a/src/librustdoc/html/render.rs b/src/librustdoc/html/render.rs index b46efb20d8f6b..ae3f57ab85ebf 100644 --- a/src/librustdoc/html/render.rs +++ b/src/librustdoc/html/render.rs @@ -859,6 +859,11 @@ themePicker.onblur = handleThemeButtonsBlur; write_minify(cx.dst.join(&format!("settings{}.js", cx.shared.resource_suffix)), static_files::SETTINGS_JS, options.enable_minification)?; + if cx.shared.include_sources { + write_minify(cx.dst.join(&format!("source-script{}.js", cx.shared.resource_suffix)), + static_files::sidebar::SOURCE_SCRIPT, + options.enable_minification)?; + } { let mut data = format!("var resourcesSuffix = \"{}\";\n", @@ -969,10 +974,88 @@ themePicker.onblur = handleThemeButtonsBlur; } } + use std::ffi::OsString; + + #[derive(Debug)] + struct Hierarchy { + elem: OsString, + children: FxHashMap, + elems: FxHashSet, + } + + impl Hierarchy { + fn new(elem: OsString) -> Hierarchy { + Hierarchy { + elem, + children: FxHashMap::default(), + elems: FxHashSet::default(), + } + } + + fn to_json_string(&self) -> String { + let mut subs: Vec<&Hierarchy> = self.children.values().collect(); + subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem)); + let mut files = self.elems.iter() + .map(|s| format!("\"{}\"", + s.to_str() + .expect("invalid osstring conversion"))) + .collect::>(); + files.sort_unstable_by(|a, b| a.cmp(b)); + // FIXME(imperio): we could avoid to generate "dirs" and "files" if they're empty. + format!("{{\"name\":\"{name}\",\"dirs\":[{subs}],\"files\":[{files}]}}", + name=self.elem.to_str().expect("invalid osstring conversion"), + subs=subs.iter().map(|s| s.to_json_string()).collect::>().join(","), + files=files.join(",")) + } + } + + if cx.shared.include_sources { + use std::path::Component; + + let mut hierarchy = Hierarchy::new(OsString::new()); + for source in cx.shared.local_sources.iter() + .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root) + .ok()) { + let mut h = &mut hierarchy; + let mut elems = source.components() + .filter_map(|s| { + match s { + Component::Normal(s) => Some(s.to_owned()), + _ => None, + } + }) + .peekable(); + loop { + let cur_elem = elems.next().expect("empty file path"); + if elems.peek().is_none() { + h.elems.insert(cur_elem); + break; + } else { + let e = cur_elem.clone(); + h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e)); + h = h.children.get_mut(&cur_elem).expect("not found child"); + } + } + } + + let dst = cx.dst.join("source-files.js"); + let (mut all_sources, _krates) = try_err!(collect(&dst, &krate.name, "sourcesIndex"), &dst); + all_sources.push(format!("sourcesIndex['{}'] = {};", + &krate.name, + hierarchy.to_json_string())); + all_sources.sort(); + let mut w = try_err!(File::create(&dst), &dst); + try_err!(writeln!(&mut w, + "var N = null;var sourcesIndex = {{}};\n{}", + all_sources.join("\n")), + &dst); + } + // Update the search index let dst = cx.dst.join("search-index.js"); let (mut all_indexes, mut krates) = try_err!(collect(&dst, &krate.name, "searchIndex"), &dst); all_indexes.push(search_index); + // Sort the indexes by crate so the file will be generated identically even // with rustdoc running in parallel. all_indexes.sort(); @@ -1020,7 +1103,7 @@ themePicker.onblur = handleThemeButtonsBlur; try_err!(layout::render(&mut w, &cx.shared.layout, &page, &(""), &content, cx.shared.css_file_extension.is_some(), - &cx.shared.themes), &dst); + &cx.shared.themes, &[]), &dst); try_err!(w.flush(), &dst); } } @@ -1292,7 +1375,8 @@ impl<'a> SourceCollector<'a> { layout::render(&mut w, &self.scx.layout, &page, &(""), &Source(contents), self.scx.css_file_extension.is_some(), - &self.scx.themes)?; + &self.scx.themes, &["source-files", + &format!("source-script{}", page.resource_suffix)])?; w.flush()?; self.scx.local_sources.insert(p.clone(), href); Ok(()) @@ -1890,7 +1974,7 @@ impl Context { try_err!(layout::render(&mut w, &self.shared.layout, &page, &sidebar, &all, self.shared.css_file_extension.is_some(), - &self.shared.themes), + &self.shared.themes, &[]), &final_file); // Generating settings page. @@ -1910,7 +1994,7 @@ impl Context { try_err!(layout::render(&mut w, &layout, &page, &sidebar, &settings, self.shared.css_file_extension.is_some(), - &themes), + &themes, &[]), &settings_file); Ok(()) @@ -1968,7 +2052,7 @@ impl Context { &Sidebar{ cx: self, item: it }, &Item{ cx: self, item: it }, self.shared.css_file_extension.is_some(), - &self.shared.themes)?; + &self.shared.themes, &[])?; } else { let mut url = self.root_path(); if let Some(&(ref names, ty)) = cache().paths.get(&it.def_id) { diff --git a/src/librustdoc/html/static/main.js b/src/librustdoc/html/static/main.js index 55415e973c50a..781f99cd6932f 100644 --- a/src/librustdoc/html/static/main.js +++ b/src/librustdoc/html/static/main.js @@ -13,6 +13,19 @@ /*jslint browser: true, es5: true */ /*globals $: true, rootPath: true */ +if (!String.prototype.startsWith) { + String.prototype.startsWith = function(searchString, position) { + position = position || 0; + return this.indexOf(searchString, position) === position; + }; +} +if (!String.prototype.endsWith) { + String.prototype.endsWith = function(suffix, length) { + var l = length || this.length; + return this.indexOf(suffix, l - suffix.length) !== -1; + }; +} + (function() { "use strict"; @@ -57,19 +70,6 @@ var titleBeforeSearch = document.title; - if (!String.prototype.startsWith) { - String.prototype.startsWith = function(searchString, position) { - position = position || 0; - return this.indexOf(searchString, position) === position; - }; - } - if (!String.prototype.endsWith) { - String.prototype.endsWith = function(suffix, length) { - var l = length || this.length; - return this.indexOf(suffix, l - suffix.length) !== -1; - }; - } - function getPageId() { var id = document.location.href.split('#')[1]; if (id) { @@ -78,46 +78,6 @@ return null; } - function hasClass(elem, className) { - if (elem && className && elem.className) { - var elemClass = elem.className; - var start = elemClass.indexOf(className); - if (start === -1) { - return false; - } else if (elemClass.length === className.length) { - return true; - } else { - if (start > 0 && elemClass[start - 1] !== ' ') { - return false; - } - var end = start + className.length; - return !(end < elemClass.length && elemClass[end] !== ' '); - } - } - return false; - } - - function addClass(elem, className) { - if (elem && className && !hasClass(elem, className)) { - if (elem.className && elem.className.length > 0) { - elem.className += ' ' + className; - } else { - elem.className = className; - } - } - } - - function removeClass(elem, className) { - if (elem && className && elem.className) { - elem.className = (" " + elem.className + " ").replace(" " + className + " ", " ") - .trim(); - } - } - - function isHidden(elem) { - return (elem.offsetParent === null) - } - function showSidebar() { var elems = document.getElementsByClassName("sidebar-elems")[0]; if (elems) { diff --git a/src/librustdoc/html/static/rustdoc.css b/src/librustdoc/html/static/rustdoc.css index b2473dd9b236d..902c492105f59 100644 --- a/src/librustdoc/html/static/rustdoc.css +++ b/src/librustdoc/html/static/rustdoc.css @@ -113,7 +113,8 @@ h3.impl, h3.method, h3.type { h1, h2, h3, h4, .sidebar, a.source, .search-input, .content table :not(code)>a, -.collapse-toggle, div.item-list .out-of-band { +.collapse-toggle, div.item-list .out-of-band, +#source-sidebar, #sidebar-toggle { font-family: "Fira Sans", sans-serif; } @@ -668,9 +669,9 @@ a { padding-right: 10px; } .content .search-results td:first-child a:after { - clear: both; - content: ""; - display: block; + clear: both; + content: ""; + display: block; } .content .search-results td:first-child a span { float: left; @@ -1459,3 +1460,68 @@ kbd { .non-exhaustive { margin-bottom: 1em; } + +#sidebar-toggle { + position: fixed; + top: 30px; + left: 300px; + z-index: 10; + padding: 3px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + cursor: pointer; + font-weight: bold; + transition: left .5s; + font-size: 1.2em; + border: 1px solid; + border-left: 0; +} +#source-sidebar { + position: fixed; + top: 0; + bottom: 0; + left: 0; + width: 300px; + z-index: 1; + overflow: auto; + transition: left .5s; + border-right: 1px solid; +} +#source-sidebar > .title { + font-size: 1.5em; + text-align: center; + border-bottom: 1px solid; + margin-bottom: 6px; +} + +div.children { + padding-left: 27px; + display: none; +} +div.name { + cursor: pointer; + position: relative; + margin-left: 16px; +} +div.files > a { + display: block; + padding: 0 3px; +} +div.files > a:hover, div.name:hover { + background-color: #a14b4b; +} +div.name.expand + .children { + display: block; +} +div.name::before { + content: "\25B6"; + padding-left: 4px; + font-size: 0.7em; + position: absolute; + left: -16px; + top: 4px; +} +div.name.expand::before { + transform: rotate(90deg); + left: -14px; +} diff --git a/src/librustdoc/html/static/source-script.js b/src/librustdoc/html/static/source-script.js new file mode 100644 index 0000000000000..1db8218dae6f2 --- /dev/null +++ b/src/librustdoc/html/static/source-script.js @@ -0,0 +1,147 @@ +/*! + * Copyright 2018 The Rust Project Developers. See the COPYRIGHT + * file at the top-level directory of this distribution and at + * http://rust-lang.org/COPYRIGHT. + * + * Licensed under the Apache License, Version 2.0 or the MIT license + * , at your + * option. This file may not be copied, modified, or distributed + * except according to those terms. + */ + +function getCurrentFilePath() { + var parts = window.location.pathname.split("/"); + var rootPathParts = window.rootPath.split("/"); + + for (var i = 0; i < rootPathParts.length; ++i) { + if (rootPathParts[i] === "..") { + parts.pop(); + } + } + var file = window.location.pathname.substring(parts.join("/").length); + if (file.startsWith("/")) { + file = file.substring(1); + } + return file.substring(0, file.length - 5); +} + +function createDirEntry(elem, parent, fullPath, currentFile, hasFoundFile) { + var name = document.createElement("div"); + name.className = "name"; + + fullPath += elem["name"] + "/"; + + name.onclick = function() { + if (hasClass(this, "expand")) { + removeClass(this, "expand"); + } else { + addClass(this, "expand"); + } + }; + name.innerText = elem["name"]; + + var children = document.createElement("div"); + children.className = "children"; + var folders = document.createElement("div"); + folders.className = "folders"; + for (var i = 0; i < elem.dirs.length; ++i) { + if (createDirEntry(elem.dirs[i], folders, fullPath, currentFile, + hasFoundFile) === true) { + addClass(name, "expand"); + hasFoundFile = true; + } + } + children.appendChild(folders); + + var files = document.createElement("div"); + files.className = "files"; + for (i = 0; i < elem.files.length; ++i) { + var file = document.createElement("a"); + file.innerText = elem.files[i]; + file.href = window.rootPath + "src/" + fullPath + elem.files[i] + ".html"; + if (hasFoundFile === false && + currentFile === fullPath + elem.files[i]) { + file.className = "selected"; + addClass(name, "expand"); + hasFoundFile = true; + } + files.appendChild(file); + } + search.fullPath = fullPath; + children.appendChild(files); + parent.appendChild(name); + parent.appendChild(children); + return hasFoundFile === true && currentFile.startsWith(fullPath); +} + +function toggleSidebar() { + var sidebar = document.getElementById("source-sidebar"); + var child = this.children[0].children[0]; + if (child.innerText === ">") { + sidebar.style.left = ""; + this.style.left = ""; + child.innerText = "<"; + updateLocalStorage("rustdoc-source-sidebar-show", "true"); + } else { + sidebar.style.left = "-300px"; + this.style.left = "0"; + child.innerText = ">"; + updateLocalStorage("rustdoc-source-sidebar-show", "false"); + } +} + +function createSidebarToggle() { + var sidebarToggle = document.createElement("div"); + sidebarToggle.id = "sidebar-toggle"; + sidebarToggle.onclick = toggleSidebar; + + var inner1 = document.createElement("div"); + inner1.style.position = "relative"; + + var inner2 = document.createElement("div"); + inner2.style.marginTop = "-2px"; + if (getCurrentValue("rustdoc-source-sidebar-show") === "true") { + inner2.innerText = "<"; + } else { + inner2.innerText = ">"; + sidebarToggle.style.left = "0"; + } + + inner1.appendChild(inner2); + sidebarToggle.appendChild(inner1); + return sidebarToggle; +} + +function createSourceSidebar() { + if (window.rootPath.endsWith("/") === false) { + window.rootPath += "/"; + } + var main = document.getElementById("main"); + + var sidebarToggle = createSidebarToggle(); + main.insertBefore(sidebarToggle, main.firstChild); + + var sidebar = document.createElement("div"); + sidebar.id = "source-sidebar"; + if (getCurrentValue("rustdoc-source-sidebar-show") !== "true") { + sidebar.style.left = "-300px"; + } + + var currentFile = getCurrentFilePath(); + var hasFoundFile = false; + + var title = document.createElement("div"); + title.className = "title"; + title.innerText = "Files"; + sidebar.appendChild(title); + Object.keys(sourcesIndex).forEach(function(key) { + sourcesIndex[key].name = key; + hasFoundFile = createDirEntry(sourcesIndex[key], sidebar, "", + currentFile, hasFoundFile); + }); + + main.insertBefore(sidebar, main.firstChild); +} + +createSourceSidebar(); diff --git a/src/librustdoc/html/static/storage.js b/src/librustdoc/html/static/storage.js index 5f7a8c75d3c5f..150001a751445 100644 --- a/src/librustdoc/html/static/storage.js +++ b/src/librustdoc/html/static/storage.js @@ -15,6 +15,46 @@ var mainTheme = document.getElementById("mainThemeStyle"); var savedHref = []; +function hasClass(elem, className) { + if (elem && className && elem.className) { + var elemClass = elem.className; + var start = elemClass.indexOf(className); + if (start === -1) { + return false; + } else if (elemClass.length === className.length) { + return true; + } else { + if (start > 0 && elemClass[start - 1] !== ' ') { + return false; + } + var end = start + className.length; + return !(end < elemClass.length && elemClass[end] !== ' '); + } + } + return false; +} + +function addClass(elem, className) { + if (elem && className && !hasClass(elem, className)) { + if (elem.className && elem.className.length > 0) { + elem.className += ' ' + className; + } else { + elem.className = className; + } + } +} + +function removeClass(elem, className) { + if (elem && className && elem.className) { + elem.className = (" " + elem.className + " ").replace(" " + className + " ", " ") + .trim(); + } +} + +function isHidden(elem) { + return (elem.offsetParent === null) +} + function onEach(arr, func, reversed) { if (arr && arr.length > 0 && func) { if (reversed !== true) { diff --git a/src/librustdoc/html/static/themes/dark.css b/src/librustdoc/html/static/themes/dark.css index 4a8950b236c62..acf9a8cca6447 100644 --- a/src/librustdoc/html/static/themes/dark.css +++ b/src/librustdoc/html/static/themes/dark.css @@ -416,3 +416,22 @@ kbd { .impl-items code { background-color: rgba(0, 0, 0, 0); } + +#sidebar-toggle { + background-color: #565656; +} +#sidebar-toggle:hover { + background-color: #676767; +} +#source-sidebar { + background-color: #565656; +} +#source-sidebar > .title { + border-bottom-color: #ccc; +} +div.files > a:hover, div.name:hover { + background-color: #444; +} +div.files > .selected { + background-color: #333; +} diff --git a/src/librustdoc/html/static/themes/light.css b/src/librustdoc/html/static/themes/light.css index b3b0b6b2ea9e8..d98f1718a6ab7 100644 --- a/src/librustdoc/html/static/themes/light.css +++ b/src/librustdoc/html/static/themes/light.css @@ -410,3 +410,22 @@ kbd { .impl-items code { background-color: rgba(0, 0, 0, 0); } + +#sidebar-toggle { + background-color: #F1F1F1; +} +#sidebar-toggle:hover { + background-color: #E0E0E0; +} +#source-sidebar { + background-color: #F1F1F1; +} +#source-sidebar > .title { + border-bottom-color: #ccc; +} +div.files > a:hover, div.name:hover { + background-color: #E0E0E0; +} +div.files > .selected { + background-color: #fff; +} diff --git a/src/librustdoc/html/static_files.rs b/src/librustdoc/html/static_files.rs index 3baa082bd0e69..ee29f15d686c1 100644 --- a/src/librustdoc/html/static_files.rs +++ b/src/librustdoc/html/static_files.rs @@ -109,3 +109,9 @@ pub mod source_code_pro { /// The file `SourceCodePro-LICENSE.txt`, the license text of the Source Code Pro font. pub static LICENSE: &'static [u8] = include_bytes!("static/SourceCodePro-LICENSE.txt"); } + +/// Files related to the sidebar in rustdoc sources. +pub mod sidebar { + /// File script to handle sidebar. + pub static SOURCE_SCRIPT: &'static str = include_str!("static/source-script.js"); +} diff --git a/src/test/rustdoc/source-file.rs b/src/test/rustdoc/source-file.rs new file mode 100644 index 0000000000000..077817bcf5b46 --- /dev/null +++ b/src/test/rustdoc/source-file.rs @@ -0,0 +1,15 @@ +// Copyright 2018 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![crate_name = "foo"] + +// @has source-files.js source-file.rs + +pub struct Foo;