mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: Support URL format publicDir in Remark Image plugin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Support URL format `publicDir` in Remark Image plugin
|
||||
@@ -23,23 +23,12 @@ Supported:
|
||||
|
||||
### How It Works
|
||||
|
||||
It transforms your `` into Next.js Image usage, and add required props like `width` and `height`.
|
||||
|
||||
By default, it uses Next.js static imports to import local images, which supports the `placeholder` option of Next.js Image.
|
||||
It transforms your `` into Next.js Image usage, and add required props like `width` and `height`.
|
||||
|
||||
By default, it uses **static imports** to import local images, which supports the `placeholder` option of Next.js Image.
|
||||
Next.js can handle image imports with its built-in image loader.
|
||||
|
||||
```mdx
|
||||
import MyImage from './public/image.png';
|
||||
|
||||
<img alt="image" src={MyImage} />
|
||||
```
|
||||
|
||||
Otherwise, it obtains the image size from file-system or HTTP request.
|
||||
|
||||
```mdx
|
||||
<img alt="image" src="/image.png" widht="1980" height="1080" />
|
||||
```
|
||||
Otherwise, it uses the file system or an HTTP request to download the image and obtain its size.
|
||||
|
||||
### Options
|
||||
|
||||
@@ -69,6 +58,8 @@ import HelloImage from './public/hello.png';
|
||||
/>
|
||||
```
|
||||
|
||||
Where `./public/hello.png` points to the image in public directory.
|
||||
|
||||
### Example: Without Imports
|
||||
|
||||
You can disable Next.js static imports on local images.
|
||||
@@ -97,3 +88,48 @@ Yields:
|
||||
height="1080"
|
||||
/>
|
||||
```
|
||||
|
||||
### Example: Relative Paths
|
||||
|
||||
When `useImport` is enabled, you can reference local images using relative paths.
|
||||
|
||||
```mdx
|
||||

|
||||
```
|
||||
|
||||
Be careful that using it with `useImport` disabled **doesn't work**.
|
||||
Next.js will not add the image to public assets unless you have imported it in code.
|
||||
For images in public directory, you can just reference them without relative paths.
|
||||
|
||||
### Example: Public Directory
|
||||
|
||||
Customise the path of public directory
|
||||
|
||||
```ts
|
||||
import { remarkImage } from 'fumadocs-core/mdx-plugins';
|
||||
import path from 'node:path';
|
||||
|
||||
export default {
|
||||
remarkPlugins: [
|
||||
remarkImage,
|
||||
{
|
||||
publicDir: path.join(process.cwd(), 'dir'),
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
You can pass a URL too.
|
||||
|
||||
```ts
|
||||
import { remarkImage } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
export default {
|
||||
remarkPlugins: [
|
||||
remarkImage,
|
||||
{
|
||||
publicDir: 'http://localhost:3000/images',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -102,3 +102,10 @@ export default function Page({ params }: { params: { slug?: string[] } }) {
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Images
|
||||
|
||||
On some platforms like Vercel, the original `public` folder (including static assets like images) will be removed after `next build`.
|
||||
`compileMDX` might no longer be able to access local images in `public`.
|
||||
|
||||
When referencing images, make sure to use a URL.
|
||||
|
||||
@@ -5,14 +5,14 @@ import { visit } from 'unist-util-visit';
|
||||
import sizeOf from 'image-size';
|
||||
import { type ISizeCalculationResult } from 'image-size/dist/types/interface';
|
||||
import type { MdxjsEsm } from 'mdast-util-mdxjs-esm';
|
||||
import slash from '@/utils/slash';
|
||||
import { resolvePath, slash } from '@/utils/path';
|
||||
|
||||
const VALID_BLUR_EXT = ['.jpeg', '.png', '.webp', '.avif', '.jpg'];
|
||||
const EXTERNAL_URL_REGEX = /^https?:\/\//;
|
||||
|
||||
export interface RemarkImageOptions {
|
||||
/**
|
||||
* Directory to resolve absolute image paths
|
||||
* Directory or base URL to resolve absolute image paths
|
||||
*/
|
||||
publicDir?: string;
|
||||
|
||||
@@ -63,27 +63,25 @@ export function remarkImage({
|
||||
|
||||
function getImportPath(src: string): string {
|
||||
if (!src.startsWith('/')) return src;
|
||||
const to = path.join(publicDir, src);
|
||||
|
||||
if (file.path) {
|
||||
const relative = path.relative(
|
||||
path.dirname(file.path),
|
||||
path.join(publicDir, src),
|
||||
);
|
||||
if (file.dirname) {
|
||||
const relative = slash(path.relative(file.dirname, to));
|
||||
|
||||
return relative.startsWith('./') ? relative : `./${relative}`;
|
||||
}
|
||||
|
||||
return path.join(publicDir, src);
|
||||
return slash(to);
|
||||
}
|
||||
|
||||
visit(tree, 'image', (node) => {
|
||||
const src = decodeURI(node.url);
|
||||
if (!src) return;
|
||||
const isExternal = EXTERNAL_URL_REGEX.test(src);
|
||||
const url = decodeURI(node.url);
|
||||
if (!url) return;
|
||||
const isExternal = EXTERNAL_URL_REGEX.test(url);
|
||||
|
||||
if ((isExternal && external) || !useImport) {
|
||||
promises.push(
|
||||
getImageSize(src, publicDir).then((size) => {
|
||||
const task = getImageSize(url, publicDir)
|
||||
.then((size) => {
|
||||
if (!size.width || !size.height) return;
|
||||
|
||||
Object.assign(node, {
|
||||
@@ -98,7 +96,7 @@ export function remarkImage({
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'src',
|
||||
value: src,
|
||||
value: url,
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
@@ -112,18 +110,24 @@ export function remarkImage({
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
console.error(
|
||||
`[Remark Image] Failed obtain image size for ${url} with public directory ${publicDir}`,
|
||||
);
|
||||
});
|
||||
|
||||
promises.push(task);
|
||||
} else if (!isExternal) {
|
||||
// Unique variable name for the given static image URL
|
||||
const variableName = `__img${importsToInject.length.toString()}`;
|
||||
const hasBlur =
|
||||
placeholder === 'blur' &&
|
||||
VALID_BLUR_EXT.some((ext) => src.endsWith(ext));
|
||||
VALID_BLUR_EXT.some((ext) => url.endsWith(ext));
|
||||
|
||||
importsToInject.push({
|
||||
variableName,
|
||||
importPath: slash(getImportPath(src)),
|
||||
importPath: getImportPath(url),
|
||||
});
|
||||
|
||||
Object.assign(node, {
|
||||
@@ -194,26 +198,24 @@ export function remarkImage({
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `src` to an absolute path
|
||||
*/
|
||||
function resolveSrc(src: string, dir: string): string {
|
||||
return src.startsWith('/') || !path.isAbsolute(src)
|
||||
? path.join(dir, src)
|
||||
: src;
|
||||
}
|
||||
|
||||
async function getImageSize(
|
||||
src: string,
|
||||
dir: string,
|
||||
): Promise<ISizeCalculationResult> {
|
||||
if (EXTERNAL_URL_REGEX.test(src)) {
|
||||
const res = await fetch(src);
|
||||
const isRelative = src.startsWith('/') || !path.isAbsolute(src);
|
||||
let url: string;
|
||||
|
||||
return sizeOf(
|
||||
await res.arrayBuffer().then((buffer) => new Uint8Array(buffer)),
|
||||
);
|
||||
if (EXTERNAL_URL_REGEX.test(src)) {
|
||||
url = src;
|
||||
} else if (EXTERNAL_URL_REGEX.test(dir) && isRelative) {
|
||||
const base = new URL(dir);
|
||||
base.pathname = resolvePath(base.pathname, src);
|
||||
url = base.toString();
|
||||
} else {
|
||||
return sizeOf(isRelative ? path.join(dir, src) : src);
|
||||
}
|
||||
|
||||
return sizeOf(resolveSrc(src, dir));
|
||||
const buffer = await fetch(url).then((res) => res.arrayBuffer());
|
||||
|
||||
return sizeOf(new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { MetaData, PageData } from '@/source/types';
|
||||
import {
|
||||
parseFilePath,
|
||||
parseFolderPath,
|
||||
splitPath,
|
||||
type FileInfo,
|
||||
} from './path';
|
||||
import { parseFilePath, parseFolderPath, type FileInfo } from './path';
|
||||
import { splitPath } from '@/utils/path';
|
||||
|
||||
export interface MetaFile {
|
||||
file: FileInfo;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { I18nConfig } from '@/i18n';
|
||||
import { removeUndefined } from '@/utils/remove-undefined';
|
||||
import type * as PageTree from '../server/page-tree';
|
||||
import type { File, Folder, MetaFile, PageFile, Storage } from './file-system';
|
||||
import { resolvePath } from './path';
|
||||
import { resolvePath } from '@/utils/path';
|
||||
import { type UrlFn } from './types';
|
||||
|
||||
interface PageTreeBuilderContext {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import slash from '@/utils/slash';
|
||||
import { slash, splitPath } from '@/utils/path';
|
||||
|
||||
export interface FileInfo {
|
||||
/**
|
||||
@@ -82,43 +82,3 @@ export function normalizePath(path: string): string {
|
||||
throw new Error("It must not start with './' or '../'");
|
||||
return segments.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Split path into segments, trailing/leading slashes are removed
|
||||
*/
|
||||
export function splitPath(path: string): string[] {
|
||||
return path.split('/').filter((p) => p.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve paths, slashes within the path will be ignored
|
||||
* @param from - Path to resolve from
|
||||
* @param join - Paths to resolve
|
||||
* @example
|
||||
* ```
|
||||
* ['a','b'] // 'a/b'
|
||||
* ['/a'] // 'a'
|
||||
* ['a', '/b'] // 'a/b'
|
||||
* ['a', '../b/c'] // 'b/c'
|
||||
* ```
|
||||
*/
|
||||
export function resolvePath(from: string, join: string): string {
|
||||
const v1 = splitPath(from),
|
||||
v2 = splitPath(join);
|
||||
|
||||
while (v2.length > 0) {
|
||||
switch (v2[0]) {
|
||||
case '..':
|
||||
v1.pop();
|
||||
break;
|
||||
case '.':
|
||||
break;
|
||||
default:
|
||||
v1.push(v2[0]);
|
||||
}
|
||||
|
||||
v2.shift();
|
||||
}
|
||||
|
||||
return v1.join('/');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Split path into segments, trailing/leading slashes are removed
|
||||
*/
|
||||
export function splitPath(path: string): string[] {
|
||||
return path.split('/').filter((p) => p.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve paths, slashes within the path will be ignored
|
||||
* @param from - Path to resolve from
|
||||
* @param join - Paths to resolve
|
||||
* @example
|
||||
* ```
|
||||
* ['a','b'] // 'a/b'
|
||||
* ['/a'] // 'a'
|
||||
* ['a', '/b'] // 'a/b'
|
||||
* ['a', '../b/c'] // 'b/c'
|
||||
* ```
|
||||
*/
|
||||
export function resolvePath(from: string, join: string): string {
|
||||
const v1 = splitPath(from),
|
||||
v2 = splitPath(join);
|
||||
|
||||
while (v2.length > 0) {
|
||||
switch (v2[0]) {
|
||||
case '..':
|
||||
v1.pop();
|
||||
break;
|
||||
case '.':
|
||||
break;
|
||||
default:
|
||||
v1.push(v2[0]);
|
||||
}
|
||||
|
||||
v2.shift();
|
||||
}
|
||||
|
||||
return v1.join('/');
|
||||
}
|
||||
|
||||
export function slash(path: string): string {
|
||||
const isExtendedLengthPath = path.startsWith('\\\\?\\');
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export default function slash(path: string): string {
|
||||
const isExtendedLengthPath = path.startsWith('\\\\?\\');
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||

|
||||
@@ -0,0 +1 @@
|
||||
<img alt="External" src="/237/200/300" width="200" height="300" />
|
||||
@@ -1,14 +1,10 @@
|
||||
import {
|
||||
resolvePath,
|
||||
parseFilePath,
|
||||
parseFolderPath,
|
||||
splitPath,
|
||||
} from '@/source/path';
|
||||
import { parseFilePath, parseFolderPath } from '@/source/path';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import type { Root } from '@/server/page-tree';
|
||||
import { findNeighbour } from '@/server/page-tree-utils';
|
||||
import { PageTree } from '../dist/server';
|
||||
import { getBreadcrumbItems } from '@/breadcrumb';
|
||||
import { resolvePath, splitPath } from '@/utils/path';
|
||||
|
||||
test('Find Neighbours', () => {
|
||||
const tree: Root = {
|
||||
|
||||
@@ -68,6 +68,22 @@ test('Remark Image', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Image: With Path', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-image.md');
|
||||
const content = readFileSync(file);
|
||||
const processor = remark()
|
||||
.use(remarkImage, { publicDir: path.resolve(cwd, './fixtures') })
|
||||
.use(remarkMdx);
|
||||
|
||||
const result = await processor.run(processor.parse(content), {
|
||||
path: file,
|
||||
});
|
||||
|
||||
expect(result).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-image.output.json'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Image: Without Import', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-image.md'));
|
||||
const result = await remark()
|
||||
@@ -83,6 +99,23 @@ test('Remark Image: Without Import', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Image: `publicDir` with URL', async () => {
|
||||
const content = readFileSync(
|
||||
path.resolve(cwd, './fixtures/remark-image-public-dir.md'),
|
||||
);
|
||||
const result = await remark()
|
||||
.use(remarkImage, {
|
||||
publicDir: 'https://picsum.photos/id',
|
||||
useImport: false,
|
||||
})
|
||||
.use(remarkMdx)
|
||||
.process(content);
|
||||
|
||||
expect(result.value).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-image-public-dir.output.mdx'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Rehype Toc', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/rehype-toc.md'));
|
||||
|
||||
|
||||
@@ -81,8 +81,6 @@ const cases = [
|
||||
|
||||
for (const { name, collection, global } of cases) {
|
||||
test(`generate JS index file: ${name}`, async () => {
|
||||
console.log(global);
|
||||
|
||||
const out = await generateJS(
|
||||
path.join(file, './fixtures/config.ts'),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user