Skip to content
Open
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
60 changes: 54 additions & 6 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,30 @@ window.$docsify = {
};
```

## absoluteBasePath

- Type: `String|null`
- Default: `null` (domain root `/`)

Overrides [`basePath`](#basepath) when resolving paths that begin with `/`.
By default, absolute links and resources are resolved from the domain root,
including when Docsify is served from a nested directory.

```js
window.$docsify = {
absoluteBasePath: '/docs/',
};
```

## basePath

- Type: `String`
- Type: `String|null`
- Default: `null`

Base path of the website. You can set it to another directory or another domain name.
Sets the source path for the website and the common base for relative and
absolute resources. [`relativeBasePath`](#relativebasepath) and
[`absoluteBasePath`](#absolutebasepath) take precedence when set. You can set
it to another directory or another domain name.

```js
window.$docsify = {
Expand All @@ -102,6 +121,18 @@ window.$docsify = {
};
```

Individual markdown links, images, and embedded resources can override the
configured base with the `:basepath` attribute:

```markdown
[Guide](guide.md ':basepath=/shared/')
![Logo](logo.png ':basepath=/assets/')
[Example](example.js ':include :basepath=/examples/')
```

An invalid base-path value is ignored and the resource falls back to its
default path, allowing the rest of the page to continue rendering.

## catchPluginErrors

- Type: `Boolean`
Expand Down Expand Up @@ -719,12 +750,29 @@ window.$docsify = {

See [Plugins](./plugins.md).

## relativeBasePath

- Type: `String|null`
- Default: `null` (directory of the current page)

Overrides [`basePath`](#basepath) when resolving relative links, images, and
embedded resources. Without this option, relative paths follow standard web
behavior and are resolved from the page where they are declared.

```js
window.$docsify = {
relativeBasePath: '/shared/',
};
```

## relativePath

- Type: `Boolean`
- Default: `false`
- Default: `true`

If **true**, links are relative to the current context.
If **true**, links are relative to the page where they are declared. Set this
to `false` only to preserve the legacy behavior that resolves relative links
from the Docsify index route.

For example, the directory structure is as follows:

Expand All @@ -751,10 +799,10 @@ config/example.md => http://domain.com/zh-cn/config/example

```js
window.$docsify = {
// Relative path enabled
// Standard relative path behavior (default)
relativePath: true,

// Relative path disabled (default value)
// Legacy index-relative behavior
relativePath: false,
};
```
Expand Down
6 changes: 4 additions & 2 deletions src/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const defaultDocsifyConfig = () => ({
alias: /** @type {Record<string, string>} */ ({}),
auto2top: false,
autoHeader: false,
basePath: '',
absoluteBasePath: /** @type {null | string} */ (null),
basePath: /** @type {null | string} */ (null),
catchPluginErrors: true,
collapseSidebarGroups: false,
cornerExternalLinkTarget:
Expand Down Expand Up @@ -51,7 +52,8 @@ const defaultDocsifyConfig = () => ({
),
onlyCover: false,
plugins: /** @type {Plugin[]} */ ([]),
relativePath: false,
relativeBasePath: /** @type {null | string} */ (null),
relativePath: true,
repo: /** @type {string} */ (''),
requestHeaders: /** @type {Record<string, string>} */ ({}),
routerMode: /** @type {'hash' | 'history'} */ 'hash',
Expand Down
22 changes: 13 additions & 9 deletions src/core/render/compiler.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { marked } from 'marked';
import { isAbsolutePath, getPath, getParentPath } from '../router/util.js';
import { isFn, cached, isPrimitive } from '../util/core.js';
import { tree as treeTpl } from './tpl.js';
import { genTree } from './gen-tree.js';
Expand All @@ -16,6 +15,7 @@ import { taskListItemCompiler } from './compiler/taskListItem.js';
import { linkCompiler } from './compiler/link.js';
import { compileMedia } from './compiler/media.js';
import { tableCellCompiler } from './compiler/tableCell.js';
import { resolveResourcePath } from './path.js';

const cachedLinks = {};

Expand Down Expand Up @@ -105,13 +105,12 @@ export class Compiler {
title = str;

if (config.include) {
if (!isAbsolutePath(href)) {
href = getPath(
this.contentBase,
getParentPath(this.router.getCurrentPath()),
href,
);
}
href = resolveResourcePath(href, {
config: this.config,
contentBase: this.contentBase,
currentPath: this.router.getCurrentPath(),
elementBasePath: config.basepath,
});

let media;
const mediaType = Array.isArray(config.type)
Expand Down Expand Up @@ -183,7 +182,12 @@ export class Compiler {
compiler: this,
});
origin.paragraph = paragraphCompiler({ renderer });
origin.image = imageCompiler({ renderer, contentBase, router });
origin.image = imageCompiler({
renderer,
contentBase,
router,
compiler: this,
});
origin.list = taskListCompiler({ renderer });
origin.listitem = taskListItemCompiler({ renderer });
origin.tablecell = tableCellCompiler({ renderer });
Expand Down
13 changes: 8 additions & 5 deletions src/core/render/compiler/image.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { escapeHtml, getAndRemoveConfig } from '../utils.js';
import { isAbsolutePath, getPath, getParentPath } from '../../router/util.js';
import { resolveResourcePath } from '../path.js';

export const imageCompiler = ({ renderer, contentBase, router }) =>
export const imageCompiler = ({ renderer, contentBase, router, compiler }) =>
(renderer.image = ({ href, title, text }) => {
let url = href;
const attrs = [];
Expand Down Expand Up @@ -38,9 +38,12 @@ export const imageCompiler = ({ renderer, contentBase, router }) =>
attrs.push(`id="${config.id}"`);
}

if (!isAbsolutePath(href)) {
url = getPath(contentBase, getParentPath(router.getCurrentPath()), href);
}
url = resolveResourcePath(href, {
config: compiler.config,
contentBase,
currentPath: router.getCurrentPath(),
elementBasePath: config.basepath,
});

return /* html */ `<img src="${escapeHtml(url)}" data-origin="${escapeHtml(href)}" alt="${escapeHtml(text)}" ${attrs.join(
' ',
Expand Down
22 changes: 20 additions & 2 deletions src/core/render/compiler/link.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { escapeHtml, getAndRemoveConfig } from '../utils.js';
import { isAbsolutePath } from '../../router/util.js';
import { resolveDocumentPath } from '../path.js';

export const linkCompiler = ({
renderer,
Expand Down Expand Up @@ -27,9 +28,26 @@ export const linkCompiler = ({
if (href === compiler.config.homepage) {
href = 'README';
}
href = router.toURL(href, null, router.getCurrentPath());
const resolved = resolveDocumentPath(href, {
config: compiler.config,
elementBasePath: config.basepath,
});

if (config.target && !isMailto) {
if (isAbsolutePath(resolved.path)) {
href = resolved.path;
attrs.push(`target="${linkTarget}"`);
if (linkRel !== '') {
attrs.push(`rel="${linkRel}"`);
}
} else {
href = router.toURL(resolved.path, null, router.getCurrentPath());

if (resolved.rooted && router.mode === 'hash') {
href = `/${href}`;
}
}

if (config.target && !isMailto && !attrs.length) {
attrs.push(`target="${linkTarget}"`);
}
} else {
Expand Down
4 changes: 3 additions & 1 deletion src/core/render/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,9 @@ export function Render(Base) {
this.router.toURL(this.route.path),
);
const activeEl = /** @type {HTMLElement | null} */ (
dom.find(`.sidebar-nav a[href="${activeElmHref}"]`)
dom.find(`.sidebar-nav a[href="${activeElmHref}"]`) ||
(this.router.mode === 'hash' &&
dom.find(`.sidebar-nav a[href="/${activeElmHref}"]`))
);

this.#addTextAsTitleAttribute('.sidebar-nav a');
Expand Down
126 changes: 126 additions & 0 deletions src/core/render/path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { getParentPath, getPath, isAbsolutePath } from '../router/util.js';

const placeholderOrigin = 'https://docsify.invalid';

function getString(value) {
return typeof value === 'string' && value ? value : null;
}

function getConfiguredBasePath(
href,
config,
elementBasePath,
ignoreExternalBasePath = false,
) {
const elementPath = getString(elementBasePath);
const resourcePath = getString(
href.startsWith('/') ? config.absoluteBasePath : config.relativeBasePath,
);
const sharedPath = getString(config.basePath);
const configuredBasePath = elementPath || resourcePath || sharedPath;

// An external basePath identifies where Docsify fetches markdown files. A
// document link must remain an SPA route so the router can perform that
// fetch, while images and includes can point at the source URL directly.
return ignoreExternalBasePath &&
!elementPath &&
!resourcePath &&
isAbsolutePath(sharedPath)
? null
: configuredBasePath;
}

/**
* Resolve a path against an explicit base while preserving URL origins.
* Invalid bases return null so callers can safely use the default behavior.
*
* @param {string} path
* @param {string} basePath
* @returns {string | null}
*/
export function resolvePathFromBase(path, basePath) {
try {
const protocolRelative = basePath.startsWith('//');
const absolute = isAbsolutePath(basePath);
const base = new URL(
basePath.endsWith('/') ? basePath : `${basePath}/`,
placeholderOrigin,
);
const url = new URL(path.replace(/^\/+/, ''), base);

if (protocolRelative) {
return url.href.replace(url.protocol, '');
}

return absolute ? url.href : `${url.pathname}${url.search}${url.hash}`;
} catch {
return null;
}
}

/**
* Resolve image and embedded-resource paths using standard web path rules.
*
* @param {string} href
* @param {object} options
* @param {Record<string, any>} options.config
* @param {string} options.contentBase
* @param {string} options.currentPath
* @param {string | string[] | undefined} options.elementBasePath
* @returns {string}
*/
export function resolveResourcePath(
href,
{ config, contentBase, currentPath, elementBasePath },
) {
if (isAbsolutePath(href)) {
return href;
}

const configuredBasePath = getConfiguredBasePath(
href,
config,
elementBasePath,
);
const configuredPath =
configuredBasePath && resolvePathFromBase(href, configuredBasePath);

if (configuredPath) {
return configuredPath;
}

if (href.startsWith('/')) {
return href;
}

const pageBase = getPath(contentBase, getParentPath(currentPath));

return resolvePathFromBase(href, pageBase) || href;
}

/**
* Resolve a document link. The boolean indicates that the link is rooted at
* the domain hierarchy rather than the current Docsify index route.
*
* @param {string} href
* @param {object} options
* @param {Record<string, any>} options.config
* @param {string | string[] | undefined} options.elementBasePath
* @returns {{path: string, rooted: boolean}}
*/
export function resolveDocumentPath(href, { config, elementBasePath }) {
const configuredBasePath = getConfiguredBasePath(
href,
config,
elementBasePath,
true,
);
const configuredPath =
configuredBasePath && resolvePathFromBase(href, configuredBasePath);

if (configuredPath) {
return { path: configuredPath, rooted: true };
}

return { path: href, rooted: href.startsWith('/') };
}
2 changes: 1 addition & 1 deletion src/core/render/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function getAndRemoveConfig(str = '') {
str = str
.replace(/^('|")/, '')
.replace(/('|")$/, '')
.replace(/(?:^|\s):([\w-]+:?)=?([\w-%]+)?/g, (m, key, value) => {
.replace(/(?:^|\s):([\w-]+:?)=?([^\s'"]+)?/g, (m, key, value) => {
if (key.indexOf(':') !== -1) {
return m;
}
Expand Down
12 changes: 10 additions & 2 deletions src/core/router/history/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class History {
}

getBasePath() {
return this.config.basePath;
return this.config.basePath || '';
}

/**
Expand All @@ -63,7 +63,15 @@ export class History {
path = config.alias ? this.#getAlias(path, config.alias) : path;
path = this.#getFileName(path, ext);
path = path === `/README${ext}` ? config.homepage || path : path;
path = isAbsolutePath(path) ? path : getPath(base, path);

const normalizedBase = base && cleanPath(`/${base.replace(/^\/+/, '')}/`);
const isWithinLocalBase =
normalizedBase &&
!isAbsolutePath(base) &&
cleanPath(`/${path.replace(/^\/+/, '')}`).startsWith(normalizedBase);

path =
isAbsolutePath(path) || isWithinLocalBase ? path : getPath(base, path);

if (isRelative) {
path = path.replace(new RegExp(`^${base}`), '');
Expand Down
Loading