Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,20 @@ window.$docsify = {
};
```

## sidebarPosition

- Type: `String`
- Default: `'left'`

Controls which side of the page displays the sidebar. Set this to `'right'` to
place the sidebar and its toggle on the right.

```js
window.$docsify = {
sidebarPosition: 'right',
};
```

## homepage

- Type: `String`
Expand Down
5 changes: 5 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ By default, the hyperlink on the current page is recognized and the content is s
// You can provide a regexp to match prefixes. In this case,
// the matching substring will be used to identify the index
pathNamespaces: /^(\/(zh-cn|ru-ru))?(\/(v1|v2))?/,

// Show where each result comes from (default: 'none')
// 'page': the page title, e.g. "Guide"
// 'breadcrumb': the sidebar path, e.g. "Basics › Guide"
resultSource: 'none',
},
};
</script>
Expand Down
1 change: 1 addition & 0 deletions src/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const defaultDocsifyConfig = () => ({
skipLink: /** @type {false | string | Record<string, string>} */ (
'Skip to main content'
),
sidebarPosition: /** @type {'left' | 'right'} */ ('left'),
subMaxLevel: 0,
vueComponents: /** @type {Record<string, TODO>} */ ({}),
vueGlobalOptions: /** @type {Record<string, TODO>} */ ({}),
Expand Down
8 changes: 5 additions & 3 deletions src/core/render/tpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,20 @@ export function corner(data, cornerExternalLinkTarget) {
* @returns {String} HTML of the main content
*/
export function main(config) {
const { hideSidebar, name } = config;
const { hideSidebar, name, sidebarPosition } = config;
const sidebarPositionClass =
sidebarPosition === 'right' ? ' sidebar-right' : '';
// const name = config.name ? config.name : '';

const aside = /* html */ hideSidebar
? ''
: `
<button class="sidebar-toggle" tabindex="-1" title="Press \\ to toggle">
<button class="sidebar-toggle${sidebarPositionClass}" tabindex="-1" title="Press \\ to toggle">
<div class="sidebar-toggle-button" tabindex="0" aria-label="Hide primary navigation" aria-keyshortcuts="Use shortcut key \\" aria-controls="__sidebar" role="button">
<span></span><span></span><span></span>
</div>
</button>
<aside id="__sidebar" class="sidebar${!isMobile() ? ' show' : ''}" tabindex="-1" role="none">
<aside id="__sidebar" class="sidebar${sidebarPositionClass}${!isMobile() ? ' show' : ''}" tabindex="-1" role="none">
${
config.name
? /* html */ `
Expand Down
123 changes: 123 additions & 0 deletions src/plugins/search/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,126 @@ import cssText from './style.css';
import { escapeHtml } from '../../core/render/utils.js';

let NO_DATA_TEXT = '';
let RESULT_SOURCE = 'none';

// Strip emoji (pictographs, flags, variation selectors, ZWJ, keycaps) from
// sidebar labels and page titles so source labels stay plain text.
function stripEmoji(text) {
return (text || '')
.replace(
/(?:[\uD83C-\uD83E][\uDC00-\uDFFF])|[\u2600-\u27BF\u2B00-\u2BFF]|\uFE0E|\uFE0F|\u200D|\u20E3/g,
'',
)
.replace(/\s+/g, ' ')
.trim();
}

// User-authored links may contain malformed percent-encoding, on which
// decodeURIComponent() throws.
function safeDecode(uri) {
try {
return decodeURIComponent(uri);
} catch {
return uri;
}
}

function findSidebarLink(url) {
const base = safeDecode((url || '').split('?')[0]);

return Docsify.dom
.findAll('.sidebar-nav a')
.find(
a => safeDecode((a.getAttribute('href') || '').split('?')[0]) === base,
);
}

// Label of a sidebar list item: its own text or link text, without the text
// of the nested list of children.
function groupLabel(li) {
for (const node of li.childNodes) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
return node.textContent.trim();
}

if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'UL') {
break;
}

const text = node.textContent.trim();

if (text) {
return text;
}
}
}

return '';
}

// Walk the sidebar tree from the link matching the result URL up to the
// root, collecting section labels along the way.
function getBreadcrumb(url) {
const link = findSidebarLink(url);

if (!link) {
return null;
}

const parts = [link.textContent.trim()];
let li = link.closest('li');

while (li) {
const parentLi = li.parentElement ? li.parentElement.closest('li') : null;

if (parentLi) {
const label = groupLabel(parentLi);

if (label) {
parts.unshift(label);
}
}

li = parentLi;
}

return parts;
}

function resultSourceHtml(post) {
if (RESULT_SOURCE === 'breadcrumb') {
const parts = getBreadcrumb(post.url);

if (parts && parts.length) {
const crumbs = parts
.map((part, i) => {
const label = escapeHtml(stripEmoji(part));
// The page itself (last segment) stands out from its sections.
return i === parts.length - 1 ? `<strong>${label}</strong>` : label;
})
.join(' › ');

return /* html */ `<p class="search-breadcrumb clamp-1">${crumbs}</p>`;
}

// The page is not in the sidebar: fall back to its page title.
return post.page
? /* html */ `<p class="search-breadcrumb clamp-1"><strong>${stripEmoji(post.page)}</strong></p>`
: '';
}

if (RESULT_SOURCE === 'page') {
// Skip the label when the matched title is the page title itself.
const page = post.page && post.page !== post.title ? post.page : '';

return page
? /* html */ `<p class="search-breadcrumb clamp-1"><strong>${stripEmoji(page)}</strong></p>`
: '';
}

return '';
}

function tpl(vm, defaultValue = '') {
const { insertAfter, insertBefore } = vm.config?.search || {};
Expand Down Expand Up @@ -59,6 +179,7 @@ function doSearch(value) {
<a href="${post.url}" title="${title}">
<p class="title clamp-1">${post.title}</p>
<p class="content clamp-2">${content}</p>
${resultSourceHtml(post)}
</a>
</div>
`;
Expand Down Expand Up @@ -141,13 +262,15 @@ export function init(opts, vm) {

const keywords = vm.router.parse().query.s || '';

RESULT_SOURCE = opts.resultSource || RESULT_SOURCE;
Docsify.dom.style(cssText);
tpl(vm, escapeHtml(keywords));
bindEvents();
keywords && setTimeout(_ => doSearch(keywords), 500);
}

export function update(opts, vm) {
RESULT_SOURCE = opts.resultSource || RESULT_SOURCE;
updatePlaceholder(opts.placeholder, vm.route.path);
updateNoData(opts.noData, vm.route.path);
}
3 changes: 3 additions & 0 deletions src/plugins/search/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { init as initSearch } from './search.js';
* keyBindings: string[];
* insertAfter?: string;
* insertBefore?: string;
* resultSource?: 'none' | 'page' | 'breadcrumb';
* }} */
const CONFIG = {
placeholder: 'Type to search',
Expand All @@ -28,6 +29,7 @@ const CONFIG = {
keyBindings: ['/', 'meta+k', 'ctrl+k'],
insertAfter: undefined, // CSS selector
insertBefore: undefined, // CSS selector
resultSource: 'none', // 'none' | 'page' | 'breadcrumb'
};

const install = function (hook, vm) {
Expand All @@ -45,6 +47,7 @@ const install = function (hook, vm) {
CONFIG.namespace = opts.namespace || CONFIG.namespace;
CONFIG.pathNamespaces = opts.pathNamespaces || CONFIG.pathNamespaces;
CONFIG.keyBindings = opts.keyBindings || CONFIG.keyBindings;
CONFIG.resultSource = opts.resultSource || CONFIG.resultSource;
}

const isAuto = CONFIG.paths === 'auto';
Expand Down
16 changes: 16 additions & 0 deletions src/plugins/search/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export function genIndex(path, content = '', router, depth, indexKey) {
const index = {};
let slug;
let title = '';
let pageTitle = '';

tokens.forEach((token, tokenIndex) => {
if (token.type === 'heading' && token.depth <= depth) {
Expand All @@ -244,6 +245,10 @@ export function genIndex(path, content = '', router, depth, indexKey) {
title = removeAtag(title.trim());
}

if (!pageTitle && title) {
pageTitle = title;
}

index[slug] = {
slug,
title: title,
Expand Down Expand Up @@ -292,6 +297,13 @@ export function genIndex(path, content = '', router, depth, indexKey) {
}
});
slugify.clear();

// Let every entry know which page it belongs to, so search results can
// show the page title next to matched section titles.
Object.values(index).forEach(item => {
item.pageTitle = pageTitle;
});

return index;
}

Expand Down Expand Up @@ -368,11 +380,15 @@ export function search(query) {
});

if (matchesScore > 0) {
const postPageTitle = post.pageTitle && post.pageTitle.trim();
const matchingPost = {
title: handlePostTitle,
content: postContent ? resultStr : '',
url: postUrl,
score: matchesScore,
page: postPageTitle
? escapeHtml(ignoreDiacriticalMarks(postPageTitle))
: '',
};

matchingResults.push(matchingPost);
Expand Down
7 changes: 7 additions & 0 deletions src/plugins/search/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@
font-size: var(--font-size-s);
}

.search .matching-post .search-breadcrumb {
margin: 0.35em 0 0 0;
color: var(--color-mono-7);
font-size: var(--font-size-s);
text-align: right;
}

.search .results-status {
margin-bottom: 0;
color: var(--color-mono-6);
Expand Down
9 changes: 8 additions & 1 deletion src/themes/shared/_app.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,19 @@ main {
> .content {
position: absolute;
inset: 0;
transition: left var(--duration-medium) ease;
transition:
left var(--duration-medium) ease,
right var(--duration-medium) ease;

body:has(.sidebar.show) & {
left: var(--sidebar-width);
}

body:has(.sidebar.sidebar-right.show) & {
right: var(--sidebar-width);
left: 0;
}

/* hideSidebar: true */
body:not:has(.sidebar) & {
position: static;
Expand Down
10 changes: 10 additions & 0 deletions src/themes/shared/_mq.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@
}
}

body:has(.sidebar.sidebar-right.show) {
.app-nav {
right: 0;
}

main > .content {
right: 0;
}
}

body:has(.app-nav-merged) {
.app-nav {
display: none;
Expand Down
5 changes: 5 additions & 0 deletions src/themes/shared/_navbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
left: var(--sidebar-width);
}

body:where(:has(.sidebar.sidebar-right.show)) & {
right: var(--sidebar-width);
left: 0;
}

a {
color: var(--navbar-link-color);
text-decoration-color: transparent;
Expand Down
22 changes: 22 additions & 0 deletions src/themes/shared/_sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@
}
}

.sidebar.sidebar-right {
right: 0;
left: auto;
translate: var(--sidebar-width);
border-right: 0;
border-left: 1px solid var(--sidebar-border-color);
}

.sidebar-nav {
li {
a.page-link {
Expand Down Expand Up @@ -283,3 +291,17 @@
}
}
}

.sidebar-toggle.sidebar-right {
right: 0;
left: auto;
justify-content: end;

body:where(:has(.sidebar.sidebar-right.show)) & {
translate: calc(0px - var(--sidebar-width));
}

.sidebar-toggle-button {
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
}
Loading
Loading