diff --git a/e2e/docs/.vuepress/components/ComponentForMarkdownImport.vue b/e2e/docs/.vuepress/components/ComponentForMarkdownImport.vue deleted file mode 100644 index 7108c4a426..0000000000 --- a/e2e/docs/.vuepress/components/ComponentForMarkdownImport.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/e2e/docs/.vuepress/components/ComponentForMarkdownImportBar.vue b/e2e/docs/.vuepress/components/ComponentForMarkdownImportBar.vue new file mode 100644 index 0000000000..0d9fa2f2f6 --- /dev/null +++ b/e2e/docs/.vuepress/components/ComponentForMarkdownImportBar.vue @@ -0,0 +1,5 @@ + diff --git a/e2e/docs/.vuepress/components/ComponentForMarkdownImportFoo.vue b/e2e/docs/.vuepress/components/ComponentForMarkdownImportFoo.vue new file mode 100644 index 0000000000..3905e34a83 --- /dev/null +++ b/e2e/docs/.vuepress/components/ComponentForMarkdownImportFoo.vue @@ -0,0 +1,5 @@ + diff --git a/e2e/docs/.vuepress/markdowns/dangling-markdown-file.md b/e2e/docs/.vuepress/markdowns/dangling-markdown-file.md new file mode 100644 index 0000000000..5bf006ada9 --- /dev/null +++ b/e2e/docs/.vuepress/markdowns/dangling-markdown-file.md @@ -0,0 +1,5 @@ +
+ +dangling markdown file + +
diff --git a/e2e/docs/README.md b/e2e/docs/README.md index eb63f0ccbf..b3586d97fe 100644 --- a/e2e/docs/README.md +++ b/e2e/docs/README.md @@ -1,3 +1,5 @@ foo ## Home H2 + +demo diff --git a/e2e/docs/markdown/vue-components.md b/e2e/docs/markdown/vue-components.md index c038c08795..f38b703d0b 100644 --- a/e2e/docs/markdown/vue-components.md +++ b/e2e/docs/markdown/vue-components.md @@ -1,8 +1,13 @@ - + + + diff --git a/e2e/tests/markdown/vue-components.spec.ts b/e2e/tests/markdown/vue-components.spec.ts index f33b1dbc68..1f3f1818d6 100644 --- a/e2e/tests/markdown/vue-components.spec.ts +++ b/e2e/tests/markdown/vue-components.spec.ts @@ -6,7 +6,10 @@ test('should render vue components correctly', async ({ page }) => { await expect(page.locator('.component-for-markdown-global p')).toHaveText( 'component for markdown global', ) - await expect(page.locator('.component-for-markdown-import p')).toHaveText( - 'component for markdown import', + await expect(page.locator('.component-for-markdown-import-foo p')).toHaveText( + 'component for markdown import foo', + ) + await expect(page.locator('.component-for-markdown-import-bar p')).toHaveText( + 'component for markdown import bar', ) }) diff --git a/packages/bundler-vite/src/plugins/index.ts b/packages/bundler-vite/src/plugins/index.ts index 0268dc0dba..9ff20c8972 100644 --- a/packages/bundler-vite/src/plugins/index.ts +++ b/packages/bundler-vite/src/plugins/index.ts @@ -1,5 +1,6 @@ export * from './vuepressBuildPlugin.js' export * from './vuepressConfigPlugin.js' export * from './vuepressDevPlugin.js' +export * from './vuepressMarkdownPlugin.js' export * from './vuepressUserConfigPlugin.js' export * from './vuepressVuePlugin.js' diff --git a/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts b/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts new file mode 100644 index 0000000000..eb072f840c --- /dev/null +++ b/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts @@ -0,0 +1,53 @@ +import type { App } from '@vuepress/core' +import { parsePageContent, renderPageSfcBlocksToVue } from '@vuepress/core' +import { path } from '@vuepress/utils' +import type { Plugin } from 'vite' + +/** + * Handle markdown transformation + */ +export const vuepressMarkdownPlugin = ({ app }: { app: App }): Plugin => ({ + name: 'vuepress:markdown', + + enforce: 'pre', + + transform(code, id) { + if (!id.endsWith('.md')) return + + // get the matched page by file path (id) + const page = app.pagesMap[id] + + // if the page content is not changed, render it to vue component directly + if (page?.content === code) { + return renderPageSfcBlocksToVue(page.sfcBlocks) + } + + // parse the markdown content to sfc blocks and render it to vue component + const { sfcBlocks } = parsePageContent({ + app, + content: code, + filePath: id, + filePathRelative: path.relative(app.dir.source(), id), + options: {}, + }) + return renderPageSfcBlocksToVue(sfcBlocks) + }, + + async handleHotUpdate(ctx) { + if (!ctx.file.endsWith('.md')) return + + // read the source code + const code = await ctx.read() + + // parse the content to sfc blocks + const { sfcBlocks } = parsePageContent({ + app, + content: code, + filePath: ctx.file, + filePathRelative: path.relative(app.dir.source(), ctx.file), + options: {}, + }) + + ctx.read = () => renderPageSfcBlocksToVue(sfcBlocks) + }, +}) diff --git a/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts b/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts index d85656c731..5688e4c2fb 100644 --- a/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts +++ b/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts @@ -11,5 +11,6 @@ export const vuepressVuePlugin = ({ options: ViteBundlerOptions }): Plugin => vuePlugin({ + include: [/\.vue$/, /\.md$/], ...options.vuePluginOptions, }) diff --git a/packages/bundler-vite/src/resolveViteConfig.ts b/packages/bundler-vite/src/resolveViteConfig.ts index 6fafcec449..f950836f92 100644 --- a/packages/bundler-vite/src/resolveViteConfig.ts +++ b/packages/bundler-vite/src/resolveViteConfig.ts @@ -5,6 +5,7 @@ import { vuepressBuildPlugin, vuepressConfigPlugin, vuepressDevPlugin, + vuepressMarkdownPlugin, vuepressUserConfigPlugin, vuepressVuePlugin, } from './plugins/index.js' @@ -31,6 +32,7 @@ export const resolveViteConfig = ({ }, plugins: [ vuepressConfigPlugin({ app, isBuild, isServer }), + vuepressMarkdownPlugin({ app }), vuepressDevPlugin({ app }), vuepressBuildPlugin({ isServer }), vuepressVuePlugin({ options }), diff --git a/packages/bundler-webpack/package.json b/packages/bundler-webpack/package.json index 28d32a8f12..893252e963 100644 --- a/packages/bundler-webpack/package.json +++ b/packages/bundler-webpack/package.json @@ -20,6 +20,7 @@ "author": "meteorlxy", "type": "module", "imports": { + "#vuepress-markdown-loader": "./dist/vuepress-markdown-loader.cjs", "#vuepress-ssr-loader": "./dist/vuepress-ssr-loader.cjs" }, "exports": { diff --git a/packages/bundler-webpack/src/build/createClientConfig.ts b/packages/bundler-webpack/src/build/createClientConfig.ts index 6266b9bda1..7f0ae415de 100644 --- a/packages/bundler-webpack/src/build/createClientConfig.ts +++ b/packages/bundler-webpack/src/build/createClientConfig.ts @@ -1,4 +1,3 @@ -import { createRequire } from 'node:module' import type { App } from '@vuepress/core' import { fs } from '@vuepress/utils' import CopyWebpackPlugin from 'copy-webpack-plugin' @@ -10,8 +9,6 @@ import { createClientBaseConfig } from '../config/index.js' import type { WebpackBundlerOptions } from '../types.js' import { createClientPlugin } from './createClientPlugin.js' -const require = createRequire(import.meta.url) - /** * Filename of the client manifest file that generated by client plugin */ @@ -27,15 +24,6 @@ export const createClientConfig = async ( isBuild: true, }) - // use internal vuepress-ssr-loader to handle SSR dependencies - config.module - .rule('vue') - .test(/\.vue$/) - .use('vuepress-ssr-loader') - .before('vue-loader') - .loader(require.resolve('#vuepress-ssr-loader')) - .end() - // vuepress client plugin, handle client assets info for ssr config .plugin('vuepress-client') diff --git a/packages/bundler-webpack/src/build/createServerConfig.ts b/packages/bundler-webpack/src/build/createServerConfig.ts index d21b75b034..600141751d 100644 --- a/packages/bundler-webpack/src/build/createServerConfig.ts +++ b/packages/bundler-webpack/src/build/createServerConfig.ts @@ -1,11 +1,8 @@ -import { createRequire } from 'node:module' import type { App } from '@vuepress/core' import type Config from 'webpack-5-chain' import { createBaseConfig } from '../config/index.js' import type { WebpackBundlerOptions } from '../types.js' -const require = createRequire(import.meta.url) - export const createServerConfig = async ( app: App, options: WebpackBundlerOptions, @@ -43,17 +40,5 @@ export const createServerConfig = async ( // do not need to minimize server bundle config.optimization.minimize(false) - // use internal vuepress-ssr-loader to handle SSR dependencies - config.module - .rule('vue') - .test(/\.vue$/) - .use('vuepress-ssr-loader') - .before('vue-loader') - .loader(require.resolve('#vuepress-ssr-loader')) - .options({ - app, - }) - .end() - return config } diff --git a/packages/bundler-webpack/src/config/createBaseConfig.ts b/packages/bundler-webpack/src/config/createBaseConfig.ts index 0b6e0accc3..b6d8bec3b0 100644 --- a/packages/bundler-webpack/src/config/createBaseConfig.ts +++ b/packages/bundler-webpack/src/config/createBaseConfig.ts @@ -52,7 +52,7 @@ export const createBaseConfig = async ({ /** * module */ - handleModule({ options, config, isBuild, isServer }) + handleModule({ app, options, config, isBuild, isServer }) /** * plugins diff --git a/packages/bundler-webpack/src/config/handleModule.ts b/packages/bundler-webpack/src/config/handleModule.ts index be33ca6a73..14d8a57b0c 100644 --- a/packages/bundler-webpack/src/config/handleModule.ts +++ b/packages/bundler-webpack/src/config/handleModule.ts @@ -1,3 +1,4 @@ +import type { App } from '@vuepress/core' import type Config from 'webpack-5-chain' import type { WebpackBundlerOptions } from '../types.js' import { handleModuleAssets } from './handleModuleAssets.js' @@ -11,11 +12,13 @@ import { handleModuleVue } from './handleModuleVue.js' * Set webpack module */ export const handleModule = ({ + app, options, config, isBuild, isServer, }: { + app: App options: WebpackBundlerOptions config: Config isBuild: boolean @@ -27,7 +30,7 @@ export const handleModule = ({ ) // vue files - handleModuleVue({ options, config, isServer }) + handleModuleVue({ app, options, config, isBuild, isServer }) // pug files, for templates handleModulePug({ config }) diff --git a/packages/bundler-webpack/src/config/handleModuleVue.ts b/packages/bundler-webpack/src/config/handleModuleVue.ts index 335159cc1b..4065ff16a7 100644 --- a/packages/bundler-webpack/src/config/handleModuleVue.ts +++ b/packages/bundler-webpack/src/config/handleModuleVue.ts @@ -1,7 +1,9 @@ import { createRequire } from 'node:module' +import type { App } from '@vuepress/core' import type { VueLoaderOptions } from 'vue-loader' import { VueLoaderPlugin } from 'vue-loader' import type Config from 'webpack-5-chain' +import type { VuepressMarkdownLoaderOptions } from '../loaders/vuepressMarkdownLoader' import type { WebpackBundlerOptions } from '../types.js' const require = createRequire(import.meta.url) @@ -10,26 +12,62 @@ const require = createRequire(import.meta.url) * Set webpack module to handle vue files */ export const handleModuleVue = ({ + app, options, config, + isBuild, isServer, }: { + app: App options: WebpackBundlerOptions config: Config + isBuild: boolean isServer: boolean }): void => { - // .vue files - config.module - .rule('vue') - .test(/\.vue$/) - // use vue-loader - .use('vue-loader') - .loader(require.resolve('vue-loader')) - .options({ - ...options.vue, - isServerBuild: isServer, - } as VueLoaderOptions) - .end() + const applyVuePipeline = ({ + rule, + isMd, + }: { + rule: Config.Rule + isMd: boolean + }): void => { + // use internal vuepress-ssr-loader to handle SSR dependencies + if (isBuild) { + rule + .use('vuepress-ssr-loader') + .loader(require.resolve('#vuepress-ssr-loader')) + .end() + } + + // use official vue-loader + rule + .use('vue-loader') + .loader(require.resolve('vue-loader')) + .options({ + ...options.vue, + isServerBuild: isServer, + } satisfies VueLoaderOptions) + .end() + + // use internal vuepress-markdown-loader to handle markdown files + if (isMd) { + rule + .use('vuepress-markdown-loader') + .loader(require.resolve('#vuepress-markdown-loader')) + .options({ app } satisfies VuepressMarkdownLoaderOptions) + .end() + } + } + + applyVuePipeline({ + rule: config.module.rule('md').test(/\.md$/), + isMd: true, + }) + + applyVuePipeline({ + rule: config.module.rule('vue').test(/\.vue$/), + isMd: false, + }) // use vue-loader plugin config.plugin('vue-loader').use(VueLoaderPlugin) diff --git a/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.cts b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.cts new file mode 100644 index 0000000000..f4b3001431 --- /dev/null +++ b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.cts @@ -0,0 +1,3 @@ +const loader = require('./vuepressMarkdownLoader.js') + +module.exports = loader.vuepressMarkdownLoader diff --git a/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts new file mode 100644 index 0000000000..d9ca435d14 --- /dev/null +++ b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts @@ -0,0 +1,37 @@ +import type { App } from '@vuepress/core' +import type { LoaderDefinitionFunction } from 'webpack' + +export interface VuepressMarkdownLoaderOptions { + app: App +} + +/** + * A webpack loader to transform markdown content to vue component + */ +export const vuepressMarkdownLoader: LoaderDefinitionFunction = + async function vuepressMarkdownLoader(source) { + // import esm dependencies + const [{ parsePageContent, renderPageSfcBlocksToVue }, { path }] = + await Promise.all([import('@vuepress/core'), import('@vuepress/utils')]) + + // get app instance from loader options + const { app } = this.getOptions() + + // get the matched page by file path + const page = app.pagesMap[this.resourcePath] + + // if the page content is not changed, render it to vue component directly + if (page?.content === source) { + return renderPageSfcBlocksToVue(page.sfcBlocks) + } + + // parse the markdown content to sfc blocks and render it to vue component + const { sfcBlocks } = parsePageContent({ + app, + content: source, + filePath: this.resourcePath, + filePathRelative: path.relative(app.dir.source(), this.resourcePath), + options: {}, + }) + return renderPageSfcBlocksToVue(sfcBlocks) + } diff --git a/packages/bundler-webpack/tsup.config.ts b/packages/bundler-webpack/tsup.config.ts index 4899bc89aa..9f8379c060 100644 --- a/packages/bundler-webpack/tsup.config.ts +++ b/packages/bundler-webpack/tsup.config.ts @@ -18,6 +18,7 @@ export default defineConfig([ { ...shared, entry: { + 'vuepress-markdown-loader': './src/loaders/vuepressMarkdownLoader.cts', 'vuepress-ssr-loader': './src/loaders/vuepressSsrLoader.cts', }, format: ['cjs'], diff --git a/packages/core/src/app/appInit.ts b/packages/core/src/app/appInit.ts index 1db287fd28..ac485cfe65 100644 --- a/packages/core/src/app/appInit.ts +++ b/packages/core/src/app/appInit.ts @@ -22,7 +22,9 @@ export const appInit = async (app: App): Promise => { app.markdown = await resolveAppMarkdown(app) // create pages - app.pages = await resolveAppPages(app) + const { pages, pagesMap } = await resolveAppPages(app) + app.pages = pages + app.pagesMap = pagesMap // plugin hook: onInitialized await app.pluginApi.hooks.onInitialized.process(app) diff --git a/packages/core/src/app/prepare/preparePageComponent.ts b/packages/core/src/app/prepare/preparePageComponent.ts index 569d63697a..e88d6123d5 100644 --- a/packages/core/src/app/prepare/preparePageComponent.ts +++ b/packages/core/src/app/prepare/preparePageComponent.ts @@ -2,14 +2,16 @@ import { renderPageSfcBlocksToVue } from '../../page/index.js' import type { App, Page } from '../../types/index.js' /** - * Generate page component temp file of a single page + * Generate page component temp file if the page does not have a source file */ export const preparePageComponent = async ( app: App, page: Page, ): Promise => { - await app.writeTemp( - page.componentFilePathRelative, - renderPageSfcBlocksToVue(page.sfcBlocks), - ) + if (page.filePath === null) { + await app.writeTemp( + page.componentFilePathRelative, + renderPageSfcBlocksToVue(page.sfcBlocks), + ) + } } diff --git a/packages/core/src/app/resolveAppPages.ts b/packages/core/src/app/resolveAppPages.ts index d8cd00bd41..33c280867b 100644 --- a/packages/core/src/app/resolveAppPages.ts +++ b/packages/core/src/app/resolveAppPages.ts @@ -7,7 +7,9 @@ const log = debug('vuepress:core/app') /** * Resolve pages for vuepress app */ -export const resolveAppPages = async (app: App): Promise => { +export const resolveAppPages = async ( + app: App, +): Promise> => { log('resolveAppPages start') // resolve page absolute file paths according to the page patterns @@ -19,9 +21,14 @@ export const resolveAppPages = async (app: App): Promise => { let hasNotFoundPage = false as boolean // create pages from files + const pagesMap: Record = {} const pages = await Promise.all( pageFilePaths.map(async (filePath) => { const page = await createPage(app, { filePath }) + // if there is a source file for the page, set it to the pages map + if (page.filePath) { + pagesMap[page.filePath] = page + } // if there is a 404 page, set the default layout to NotFound if (page.path === '/404.html') { page.frontmatter.layout ??= 'NotFound' @@ -44,5 +51,5 @@ export const resolveAppPages = async (app: App): Promise => { log('resolveAppPages finish') - return pages + return { pages, pagesMap } } diff --git a/packages/core/src/page/createPage.ts b/packages/core/src/page/createPage.ts index c000873c84..0c48399162 100644 --- a/packages/core/src/page/createPage.ts +++ b/packages/core/src/page/createPage.ts @@ -85,6 +85,8 @@ export const createPage = async ( const { componentFilePath, componentFilePathRelative } = resolvePageComponentInfo({ app, + filePath, + filePathRelative, htmlFilePathRelative, }) diff --git a/packages/core/src/page/resolvePageComponentInfo.ts b/packages/core/src/page/resolvePageComponentInfo.ts index b1ee312cc4..2105bdb187 100644 --- a/packages/core/src/page/resolvePageComponentInfo.ts +++ b/packages/core/src/page/resolvePageComponentInfo.ts @@ -6,15 +6,26 @@ import type { App } from '../types/index.js' */ export const resolvePageComponentInfo = ({ app, + filePath, + filePathRelative, htmlFilePathRelative, }: { app: App + filePath: string | null + filePathRelative: string | null htmlFilePathRelative: string }): { componentFilePath: string componentFilePathRelative: string } => { - // resolve component file path + // if there is a source file for the page, use it as the component file + if (filePath && filePathRelative) { + return { + componentFilePath: filePath, + componentFilePathRelative: filePathRelative, + } + } + // otherwise, generate a component file for the page const componentFilePathRelative = path.join( 'pages', `${htmlFilePathRelative}.vue`, diff --git a/packages/core/src/types/app/app.ts b/packages/core/src/types/app/app.ts index e943950ec9..3bab071811 100644 --- a/packages/core/src/types/app/app.ts +++ b/packages/core/src/types/app/app.ts @@ -79,6 +79,13 @@ export interface App { * Only available after initialization */ pages: Page[] + + /** + * Page source filepath map. + * + * Only available after initialization + */ + pagesMap: Record } /** diff --git a/packages/core/tests/app/resolveAppPages.spec.ts b/packages/core/tests/app/resolveAppPages.spec.ts index f394888238..a0366c1ab3 100644 --- a/packages/core/tests/app/resolveAppPages.spec.ts +++ b/packages/core/tests/app/resolveAppPages.spec.ts @@ -13,7 +13,7 @@ describe('core > app > resolveAppPages', () => { }) app.markdown = createMarkdown() - const pages = await resolveAppPages(app) + const { pages } = await resolveAppPages(app) const fooPage = pages.find((page) => page.path === '/foo.html') const barPage = pages.find((page) => page.path === '/bar.html') const notFoundPage = pages.find((page) => page.path === '/404.html') @@ -33,7 +33,7 @@ describe('core > app > resolveAppPages', () => { }) app.markdown = createMarkdown() - const pages = await resolveAppPages(app) + const { pages } = await resolveAppPages(app) const fooPage = pages.find((page) => page.path === '/foo.html') const barPage = pages.find((page) => page.path === '/bar.html') const notFoundPage = pages.find((page) => page.path === '/404.html') @@ -61,7 +61,7 @@ describe('core > app > resolveAppPages', () => { app.pluginApi.registerHooks() app.markdown = createMarkdown() - const pages = await resolveAppPages(app) + const { pages } = await resolveAppPages(app) pages.forEach((page) => { expect(page.frontmatter.foo).toBe('bar') @@ -84,7 +84,7 @@ describe('core > app > resolveAppPages', () => { app.pluginApi.registerHooks() app.markdown = createMarkdown() - const pages = await resolveAppPages(app) + const { pages } = await resolveAppPages(app) pages.forEach((page) => { expect(page.frontmatter.foo).toBe('baz') diff --git a/packages/core/tests/page/resolvePageComponentInfo.spec.ts b/packages/core/tests/page/resolvePageComponentInfo.spec.ts index 7feabdd495..c8a009e1ea 100644 --- a/packages/core/tests/page/resolvePageComponentInfo.spec.ts +++ b/packages/core/tests/page/resolvePageComponentInfo.spec.ts @@ -9,9 +9,11 @@ const app = createBaseApp({ bundler: {} as Bundler, }) -it('should resolve page component info correctly', () => { +it('should resolve page component info correctly without source file path', () => { const resolved = resolvePageComponentInfo({ app, + filePath: null, + filePathRelative: null, htmlFilePathRelative: 'foo.html', }) @@ -20,3 +22,17 @@ it('should resolve page component info correctly', () => { componentFilePathRelative: 'pages/foo.html.vue', }) }) + +it('should resolve page component info correctly with source file path', () => { + const resolved = resolvePageComponentInfo({ + app, + filePath: app.dir.source('foo.md'), + filePathRelative: 'foo.md', + htmlFilePathRelative: 'foo.html', + }) + + expect(resolved).toEqual({ + componentFilePath: app.dir.source('foo.md'), + componentFilePathRelative: 'foo.md', + }) +}) diff --git a/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts b/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts index 1434888055..2524963372 100644 --- a/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts +++ b/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts @@ -8,11 +8,6 @@ export interface AssetsPluginOptions { * Whether to prepend base to absolute path */ absolutePathPrependBase?: boolean - - /** - * Prefix to add to relative assets links - */ - relativePathPrefix?: string } /** @@ -20,10 +15,7 @@ export interface AssetsPluginOptions { */ export const assetsPlugin: PluginWithOptions = ( md, - { - absolutePathPrependBase = false, - relativePathPrefix = '@source', - }: AssetsPluginOptions = {}, + { absolutePathPrependBase = false }: AssetsPluginOptions = {}, ) => { // wrap raw image renderer rule const rawImageRule = md.renderer.rules.image! @@ -35,10 +27,7 @@ export const assetsPlugin: PluginWithOptions = ( if (link) { // replace the original link with resolved link - token.attrSet( - 'src', - resolveLink(link, { env, absolutePathPrependBase, relativePathPrefix }), - ) + token.attrSet('src', resolveLink(link, { env, absolutePathPrependBase })) } return rawImageRule(tokens, idx, options, env, self) @@ -57,7 +46,6 @@ export const assetsPlugin: PluginWithOptions = ( `${prefix}${quote}${resolveLink(src.trim(), { env, absolutePathPrependBase, - relativePathPrefix, strict: true, })}${quote}`, ) @@ -74,7 +62,6 @@ export const assetsPlugin: PluginWithOptions = ( `${resolveLink(url.trim(), { env, absolutePathPrependBase, - relativePathPrefix, strict: true, })}${descriptor.replace(/[ \n]+/g, ' ').trimEnd()}`, ), diff --git a/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts b/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts index b9277ed855..a1081fe9ed 100644 --- a/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts +++ b/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts @@ -5,18 +5,12 @@ import type { MarkdownEnv } from '../../types.js' interface ResolveLinkOptions { env: MarkdownEnv absolutePathPrependBase?: boolean - relativePathPrefix: string strict?: boolean } export const resolveLink = ( link: string, - { - env, - absolutePathPrependBase = false, - relativePathPrefix, - strict = false, - }: ResolveLinkOptions, + { env, absolutePathPrependBase = false }: ResolveLinkOptions, ): string => { // do not resolve data uri if (link.startsWith('data:')) return link @@ -24,22 +18,6 @@ export const resolveLink = ( // decode link to ensure bundler can find the file correctly let resolvedLink = decode(link) - // check if the link is relative path - const isRelativePath = strict - ? // in strict mode, only link that starts with `./` or `../` is considered as relative path - /^\.{1,2}\//.test(link) - : // in non-strict mode, link that does not start with `/` and does not have protocol is considered as relative path - !link.startsWith('/') && !/[A-z]+:\/\//.test(link) - - // if the link is relative path, and the `env.filePathRelative` exists - // add `@source` alias to the link - if (isRelativePath && env.filePathRelative) { - resolvedLink = `${relativePathPrefix}/${path.join( - path.dirname(env.filePathRelative), - resolvedLink, - )}` - } - // prepend base to absolute path if needed if (absolutePathPrependBase && env.base && link.startsWith('/')) { resolvedLink = path.join(env.base, resolvedLink) diff --git a/packages/markdown/tests/plugins/assetsPlugin.spec.ts b/packages/markdown/tests/plugins/assetsPlugin.spec.ts index af149f84d9..34dca2e41d 100644 --- a/packages/markdown/tests/plugins/assetsPlugin.spec.ts +++ b/packages/markdown/tests/plugins/assetsPlugin.spec.ts @@ -49,37 +49,36 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { md: MarkdownIt().use(assetsPlugin), env: { base: '/base/', - filePathRelative: 'sub/foo.md', }, expected: [ // relative paths - 'foo', - 'foo2', - 'foo-bar', - 'foo-bar2', - 'baz', - 'out', - '汉字', - '100%', + 'foo', + 'foo2', + 'foo-bar', + 'foo-bar2', + 'baz', + 'out', + '汉字', + '100%', // absolute paths 'absolute', 'absolute-foo', // no-prefix paths - 'no-prefix', - 'no-prefix-foo', - 'alias', - '汉字', - '100%', - '~alias', - '~汉字', - '~100%', + 'no-prefix', + 'no-prefix-foo', + 'alias', + '汉字', + '100%', + '~alias', + '~汉字', + '~100%', // keep as is 'url', 'empty', // invalid paths - 'invalid', - '汉字', - '100%', + 'invalid', + '汉字', + '100%', // data uri 'data-uri', ], @@ -91,87 +90,7 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { }), env: { base: '/base/', - filePathRelative: 'sub/foo.md', }, - expected: [ - // relative paths - 'foo', - 'foo2', - 'foo-bar', - 'foo-bar2', - 'baz', - 'out', - '汉字', - '100%', - // absolute paths - 'absolute', - 'absolute-foo', - // no-prefix paths - 'no-prefix', - 'no-prefix-foo', - 'alias', - '汉字', - '100%', - '~alias', - '~汉字', - '~100%', - // keep as is - 'url', - 'empty', - // invalid paths - 'invalid', - '汉字', - '100%', - // data uri - 'data-uri', - ], - }, - { - description: 'should respect `relativePathPrefix` option', - md: MarkdownIt().use(assetsPlugin, { - relativePathPrefix: '@foo', - }), - env: { - filePathRelative: 'sub/foo.md', - }, - expected: [ - // relative paths - 'foo', - 'foo2', - 'foo-bar', - 'foo-bar2', - 'baz', - 'out', - '汉字', - '100%', - // absolute paths - 'absolute', - 'absolute-foo', - // no-prefix paths - 'no-prefix', - 'no-prefix-foo', - 'alias', - '汉字', - '100%', - '~alias', - '~汉字', - '~100%', - // keep as is - 'url', - 'empty', - // invalid paths - 'invalid', - '汉字', - '100%', - // data uri - 'data-uri', - ], - }, - { - description: - 'should not handle relative paths if `env.filePathRelative` is not provided', - md: MarkdownIt().use(assetsPlugin), - env: {}, expected: [ // relative paths 'foo', @@ -183,8 +102,8 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { '汉字', '100%', // absolute paths - 'absolute', - 'absolute-foo', + 'absolute', + 'absolute-foo', // no-prefix paths 'no-prefix', 'no-prefix-foo', @@ -302,20 +221,20 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { description: 'should handle assets link with default options', md: MarkdownIt({ html: true }).use(assetsPlugin), env: { - filePathRelative: 'sub/foo.md', + base: '/base/', }, expected: [ /* src */ // relative paths - '', - '', - '', - '', - '', - '', - '', - '', - 'attrs', + '', + '', + '', + '', + '', + '', + '', + '', + 'attrs', // aliases '', '', @@ -344,34 +263,35 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { /* srcset */ // relative paths - '', - '', - 'attrs', + '', + '', + 'attrs', // aliases '', 'attrs', // webpack legacy aliases '', 'attrs', - // keep as is + // absolute paths and no-prefix paths '', + // keep as is '', 'attrs', // invalid paths '', 'attrs', // invalid srcset - '', + '', /* both */ // relative paths - '', - '', - 'attrs', + '', + '', + 'attrs', // aliases 'attrs', 'attrs', - // keep as is + // absolute paths and no-prefix paths 'attrs', /* data uri */ @@ -379,92 +299,13 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { ], }, { - description: 'should respect `relativePathPrefix` option', + description: 'should respect `absolutePathPrependBase` option', md: MarkdownIt({ html: true }).use(assetsPlugin, { - relativePathPrefix: '@foo', + absolutePathPrependBase: true, }), env: { - filePathRelative: 'sub/foo.md', + base: '/base/', }, - expected: [ - /* src */ - // relative paths - '', - '', - '', - '', - '', - '', - '', - '', - 'attrs', - // aliases - '', - '', - '', - 'attrs', - // webpack legacy aliases - '', - '', - '', - 'attrs', - // absolute paths - '', - '', - // no-prefix paths - '', - '', - 'attrs', - // keep as is - '', - '', - // invalid paths - '', - '', - '', - 'attrs', - - /* srcset */ - // relative paths - '', - '', - 'attrs', - // aliases - '', - 'attrs', - // webpack legacy aliases - '', - 'attrs', - // keep as is - '', - '', - 'attrs', - // invalid paths - '', - 'attrs', - // invalid srcset - '', - - /* both */ - // relative paths - '', - '', - 'attrs', - // aliases - 'attrs', - 'attrs', - // keep as is - 'attrs', - - /* data uri */ - '', - ], - }, - { - description: - 'should not handle relative paths if `env.filePathRelative` is not provided', - md: MarkdownIt({ html: true }).use(assetsPlugin), - env: {}, expected: [ /* src */ // relative paths @@ -488,8 +329,8 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { '', 'attrs', // absolute paths - '', - '', + '', + '', // no-prefix paths '', '', @@ -514,8 +355,9 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { // webpack legacy aliases '', 'attrs', + // absolute paths and no-prefix paths + '', // keep as is - '', '', 'attrs', // invalid paths @@ -532,8 +374,8 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { // aliases 'attrs', 'attrs', - // keep as is - 'attrs', + // absolute paths and no-prefix paths + 'attrs', /* data uri */ '', @@ -630,22 +472,25 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { `attrs`, + `attrs`, /* srcset */ ``, + 1024w ,../../out.png, /absolute/attrs.png">`, `attrs`, + ,default.png ,/absolute/attrs.png" width="100px">`, /** both */ ``, + 1024w ,../../out.png, /absolute/attrs.png">`, `attrs`, + ,default.png ,/absolute/attrs.png" width="100px">`, ] const TEST_CASES: { @@ -658,58 +503,42 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => { description: 'should handle assets link with default options', md: MarkdownIt({ html: true }).use(assetsPlugin), env: { - filePathRelative: 'sub/foo.md', + base: '/base/', }, expected: [ /* src */ '

attrs

', + '

attrs

', /* srcset */ - '

', - '

attrs

', + '

', + '

attrs

', /* both */ - '

', - '

attrs

', + '

', + '

attrs

', ], }, { - description: 'should respect `relativePathPrefix` option', + description: 'should respect `absolutePathPrependBase` option', md: MarkdownIt({ html: true }).use(assetsPlugin, { - relativePathPrefix: '@foo', + absolutePathPrependBase: true, }), env: { - filePathRelative: 'sub/foo.md', + base: '/base/', }, expected: [ /* src */ '

attrs

', + '

attrs

', /* srcset */ - '

', - '

attrs

', - - /* both */ - '

', - '

attrs

', - ], - }, - { - description: - 'should not handle assets link if `filePathRelative` is not provided', - md: MarkdownIt({ html: true }).use(assetsPlugin), - env: {}, - expected: [ - /* src */ - '

attrs

', - - /* srcset */ - '

', - '

attrs

', + '

', + '

attrs

', /* both */ - '

', - '

attrs

', + '

', + '

attrs

', ], }, ]