mirror of
https://github.com/docsifyjs/docsify.git
synced 2026-10-18 13:11:11 +08:00
fix(router): standardize link and resource path resolution
- resolve relative paths from the current Markdown page - resolve absolute paths from the domain root - add configurable and per-element base path overrides - prevent duplicate base paths and preserve sidebar activation - add regression tests and configuration documentation Fixes #1891
This commit is contained in:
+54
-6
@@ -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/')
|
||||

|
||||
[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,
|
||||
};
|
||||
```
|
||||
|
||||
+4
-2
@@ -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:
|
||||
@@ -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',
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 */ `<img src="${escapeHtml(url)}" data-origin="${escapeHtml(href)}" alt="${escapeHtml(text)}" ${attrs.join(
|
||||
' ',
|
||||
|
||||
@@ -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,
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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('/') };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`), '');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,7 @@ exports[`Docs Site coverpage renders and is unchanged 1`] = `
|
||||
</section>"
|
||||
`;
|
||||
|
||||
exports[`Docs Site navbar renders and is unchanged 1`] = `"<nav class="app-nav" aria-label="secondary"><!-- markdownlint-disable first-line-h1 --><ul><li><p>Translations</p><ul><li><a href="#/">English</a></li><li><a href="#/zh-cn/">简体中文</a></li></ul></li></ul></nav>"`;
|
||||
exports[`Docs Site navbar renders and is unchanged 1`] = `"<nav class="app-nav" aria-label="secondary"><!-- markdownlint-disable first-line-h1 --><ul><li><p>Translations</p><ul><li><a href="/#/">English</a></li><li><a href="/#/zh-cn/">简体中文</a></li></ul></li></ul></nav>"`;
|
||||
|
||||
exports[`Docs Site sidebar renders and is unchanged 1`] = `
|
||||
"<aside id="__sidebar" class="sidebar show" tabindex="-1" role="none">
|
||||
|
||||
@@ -5,6 +5,54 @@ import { waitForText } from '../helpers/wait-for.js';
|
||||
// Suite
|
||||
// -----------------------------------------------------------------------------
|
||||
describe('render', function () {
|
||||
describe('path handling', function () {
|
||||
beforeEach(async () => {
|
||||
await docsifyInit({
|
||||
config: {
|
||||
basePath: null,
|
||||
routes: {
|
||||
'/': '# Home',
|
||||
},
|
||||
},
|
||||
testURL: `${process.env.TEST_HOST}/site/`,
|
||||
});
|
||||
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'',
|
||||
`${process.env.TEST_HOST}/site/#/dir/page`,
|
||||
);
|
||||
});
|
||||
|
||||
test('uses standard relative and absolute paths for all resources', () => {
|
||||
const output = window.marked(stripIndent`
|
||||
[relative link](guide.md)
|
||||
|
||||
[absolute link](/guide.md)
|
||||
|
||||

|
||||
|
||||

|
||||
`);
|
||||
|
||||
expect(output).toContain('href="#/dir/guide"');
|
||||
expect(output).toContain('href="/#/guide"');
|
||||
expect(output).toContain('src="/site/dir/image.png"');
|
||||
expect(output).toContain('src="/image.png"');
|
||||
});
|
||||
|
||||
test('supports per-element base paths', () => {
|
||||
const output = window.marked(stripIndent`
|
||||
[based link](guide.md ':basepath=/shared/')
|
||||
|
||||

|
||||
`);
|
||||
|
||||
expect(output).toContain('href="/#/shared/guide"');
|
||||
expect(output).toContain('src="/assets/image.png"');
|
||||
});
|
||||
});
|
||||
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('callouts', () => {
|
||||
@@ -356,7 +404,7 @@ Text</p></div>"
|
||||
const output = window.marked("[alt text](/url ':target=_blank')");
|
||||
|
||||
expect(output).toMatchInlineSnapshot(
|
||||
'"<p><a href="#/url" target="_blank">alt text</a></p>"',
|
||||
'"<p><a href="/#/url" target="_blank">alt text</a></p>"',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
resolveDocumentPath,
|
||||
resolvePathFromBase,
|
||||
resolveResourcePath,
|
||||
} from '../../src/core/render/path.js';
|
||||
|
||||
describe('render/path', () => {
|
||||
describe('resolvePathFromBase', () => {
|
||||
test.each([
|
||||
['/dir/image.png', '/', '/dir/image.png'],
|
||||
['/dir/image.png', '/site/', '/site/dir/image.png'],
|
||||
['../image.png', '/site/dir/', '/site/image.png'],
|
||||
[
|
||||
'image.png',
|
||||
'https://cdn.example.com/assets/',
|
||||
'https://cdn.example.com/assets/image.png',
|
||||
],
|
||||
])('resolves %s from %s', (path, basePath, expected) => {
|
||||
expect(resolvePathFromBase(path, basePath)).toBe(expected);
|
||||
});
|
||||
|
||||
test('returns null for an invalid base', () => {
|
||||
expect(resolvePathFromBase('image.png', 'https://[')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveResourcePath', () => {
|
||||
const defaults = {
|
||||
config: {},
|
||||
contentBase: '/site/',
|
||||
currentPath: '/dir/page',
|
||||
};
|
||||
|
||||
test.each([
|
||||
['/image.png', '/image.png'],
|
||||
['/dir/image.png', '/dir/image.png'],
|
||||
['image.png', '/site/dir/image.png'],
|
||||
['./image.png', '/site/dir/image.png'],
|
||||
['../image.png', '/site/image.png'],
|
||||
['../dir/image.png', '/site/dir/image.png'],
|
||||
])('resolves %s using standard web behavior', (href, expected) => {
|
||||
expect(resolveResourcePath(href, defaults)).toBe(expected);
|
||||
});
|
||||
|
||||
test('uses resource-specific base paths before basePath', () => {
|
||||
expect(
|
||||
resolveResourcePath('/image.png', {
|
||||
...defaults,
|
||||
config: {
|
||||
absoluteBasePath: '/assets/',
|
||||
basePath: '/ignored/',
|
||||
},
|
||||
}),
|
||||
).toBe('/assets/image.png');
|
||||
|
||||
expect(
|
||||
resolveResourcePath('image.png', {
|
||||
...defaults,
|
||||
config: {
|
||||
relativeBasePath: '/assets/',
|
||||
basePath: '/ignored/',
|
||||
},
|
||||
}),
|
||||
).toBe('/assets/image.png');
|
||||
});
|
||||
|
||||
test('uses the element base path and falls back when it is invalid', () => {
|
||||
expect(
|
||||
resolveResourcePath('image.png', {
|
||||
...defaults,
|
||||
elementBasePath: '/assets/',
|
||||
}),
|
||||
).toBe('/assets/image.png');
|
||||
|
||||
expect(
|
||||
resolveResourcePath('image.png', {
|
||||
...defaults,
|
||||
elementBasePath: true,
|
||||
}),
|
||||
).toBe('/site/dir/image.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDocumentPath', () => {
|
||||
test('preserves default relative and absolute paths', () => {
|
||||
expect(resolveDocumentPath('guide.md', { config: {} })).toEqual({
|
||||
path: 'guide.md',
|
||||
rooted: false,
|
||||
});
|
||||
expect(resolveDocumentPath('/guide.md', { config: {} })).toEqual({
|
||||
path: '/guide.md',
|
||||
rooted: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('supports a per-element base path', () => {
|
||||
expect(
|
||||
resolveDocumentPath('guide.md', {
|
||||
config: {},
|
||||
elementBasePath: '/shared/',
|
||||
}),
|
||||
).toEqual({ path: '/shared/guide.md', rooted: true });
|
||||
});
|
||||
|
||||
test('keeps external basePath links as SPA routes', () => {
|
||||
expect(
|
||||
resolveDocumentPath('guide.md', {
|
||||
config: { basePath: 'https://cdn.example.com/docs/' },
|
||||
}),
|
||||
).toEqual({ path: 'guide.md', rooted: false });
|
||||
});
|
||||
|
||||
test('allows resource-specific external bases', () => {
|
||||
expect(
|
||||
resolveDocumentPath('guide.md', {
|
||||
config: { relativeBasePath: 'https://docs.example.com/' },
|
||||
}),
|
||||
).toEqual({
|
||||
path: 'https://docs.example.com/guide.md',
|
||||
rooted: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -62,6 +62,20 @@ describe('core/render/utils', () => {
|
||||
// getAndRemoveConfig()
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('getAndRemoveConfig()', () => {
|
||||
test('parses URL paths in config values', () => {
|
||||
const result = getAndRemoveConfig(
|
||||
'image title :basepath=/assets/images/ :class=thumbnail',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
str: 'image title',
|
||||
config: {
|
||||
basepath: '/assets/images/',
|
||||
class: 'thumbnail',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('parse simple config', () => {
|
||||
const result = getAndRemoveConfig(
|
||||
"[filename](_media/example.md ':include')",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { History } from '../../src/core/router/history/base.js';
|
||||
|
||||
class MockHistory extends History {
|
||||
mode = 'hash';
|
||||
|
||||
parse(path) {
|
||||
return { path };
|
||||
}
|
||||
@@ -67,6 +69,21 @@ describe('router/history/base', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('default path behavior', () => {
|
||||
beforeEach(() => {
|
||||
history = new MockHistory({ relativePath: true });
|
||||
});
|
||||
|
||||
test('resolves relative links from the current page', () => {
|
||||
expect(history.toURL('guide.md', {}, '/dir/page')).toBe('/dir/guide');
|
||||
expect(history.toURL('../guide.md', {}, '/dir/page')).toBe('/guide');
|
||||
});
|
||||
|
||||
test('keeps absolute links rooted', () => {
|
||||
expect(history.toURL('/guide.md', {}, '/dir/page')).toBe('/guide');
|
||||
});
|
||||
});
|
||||
|
||||
// getFile test
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('getFile', () => {
|
||||
@@ -96,5 +113,11 @@ describe('router/history/base', () => {
|
||||
|
||||
expect(file).toBe('https://some/raw/url/README.md.ext');
|
||||
});
|
||||
|
||||
test('does not duplicate a configured local base path', () => {
|
||||
history = new MockHistory({ basePath: '/docs/' });
|
||||
|
||||
expect(history.getFile('/docs/guide')).toBe('/docs/guide.md');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user