diff --git a/docs/configuration.md b/docs/configuration.md index 5ad0b8958..9c678e3d2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 = { @@ -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` @@ -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: @@ -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, }; ``` diff --git a/src/core/config.js b/src/core/config.js index 4df2e29f3..a7a46b4a0 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -9,7 +9,8 @@ const defaultDocsifyConfig = () => ({ alias: /** @type {Record} */ ({}), auto2top: false, autoHeader: false, - basePath: '', + absoluteBasePath: /** @type {null | string} */ (null), + basePath: /** @type {null | string} */ (null), catchPluginErrors: true, collapseSidebarGroups: false, cornerExternalLinkTarget: @@ -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} */ ({}), routerMode: /** @type {'hash' | 'history'} */ 'hash', diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js index e81aaa81f..e0afc4477 100644 --- a/src/core/render/compiler.js +++ b/src/core/render/compiler.js @@ -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'; @@ -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 = {}; @@ -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) @@ -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 }); diff --git a/src/core/render/compiler/image.js b/src/core/render/compiler/image.js index 68bb478b4..8734a9bc3 100644 --- a/src/core/render/compiler/image.js +++ b/src/core/render/compiler/image.js @@ -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 = []; @@ -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 */ `${escapeHtml(text)}} 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} 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('/') }; +} diff --git a/src/core/render/utils.js b/src/core/render/utils.js index 2fd98e276..f84bd56d5 100644 --- a/src/core/render/utils.js +++ b/src/core/render/utils.js @@ -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; } diff --git a/src/core/router/history/base.js b/src/core/router/history/base.js index 22f2276f4..f7f15cca9 100644 --- a/src/core/router/history/base.js +++ b/src/core/router/history/base.js @@ -48,7 +48,7 @@ export class History { } getBasePath() { - return this.config.basePath; + return this.config.basePath || ''; } /** @@ -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}`), ''); diff --git a/src/core/router/history/hash.js b/src/core/router/history/hash.js index 8bd091d53..aae80b5f3 100644 --- a/src/core/router/history/hash.js +++ b/src/core/router/history/hash.js @@ -13,7 +13,7 @@ export class HashHistory extends History { getBasePath() { const path = window.location.pathname || ''; - const base = this.config.basePath; + const base = this.config.basePath || ''; // This handles the case where Docsify is served off an // explicit file path, i.e.`/base/index.html#/blah`. This diff --git a/test/integration/__snapshots__/docs.test.js.snap b/test/integration/__snapshots__/docs.test.js.snap index 85027adb9..3794218d1 100644 --- a/test/integration/__snapshots__/docs.test.js.snap +++ b/test/integration/__snapshots__/docs.test.js.snap @@ -10,7 +10,7 @@ exports[`Docs Site coverpage renders and is unchanged 1`] = ` " `; -exports[`Docs Site navbar renders and is unchanged 1`] = `""`; +exports[`Docs Site navbar renders and is unchanged 1`] = `""`; exports[`Docs Site sidebar renders and is unchanged 1`] = ` "