mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
Core: Add remark-image plugin
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
---
|
||||
'fumadocs-mdx': major
|
||||
---
|
||||
|
||||
**Enable `remark-image` plugin by default**
|
||||
|
||||
You can add image embeds easily. They will be converted to static image imports.
|
||||
|
||||
```mdx
|
||||

|
||||
```
|
||||
|
||||
Become:
|
||||
|
||||
```mdx
|
||||
import img_banner from "../../public/image.png"
|
||||
|
||||
<img alt='banner' src={img_banner} />
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': minor
|
||||
---
|
||||
|
||||
Add `remark-image` plugin that converts relative image urls into static image imports (Inspired by Nextra)
|
||||
@@ -3,8 +3,6 @@ title: Image Optimization
|
||||
description: Use Next.js image optimization in your MDX documents
|
||||
---
|
||||
|
||||
import Banner from '@/public/banner.png';
|
||||
|
||||
Contentlayer integration supports the built-in `next/image` component by passing `width`
|
||||
and `height` property automatically. You can enjoy the benefits of Next.js Image
|
||||
Optimization without extra configurations.
|
||||
@@ -24,4 +22,4 @@ Assume the image is under the `/public` folder.
|
||||

|
||||
```
|
||||
|
||||
<Image alt="Banner" src={Banner} />
|
||||

|
||||
|
||||
@@ -14,7 +14,6 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
pre: (props) => (
|
||||
<Pre {...props} ref={undefined} pre={{ className: 'max-h-[400px]' }} />
|
||||
),
|
||||
Image: defaultComponents.img,
|
||||
Tabs,
|
||||
Tab,
|
||||
Callout,
|
||||
|
||||
@@ -7,9 +7,9 @@ import remarkMath from 'remark-math';
|
||||
const withAnalyzer = createBundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
|
||||
reactStrictMode: true,
|
||||
eslint: {
|
||||
// Replaced by root workspace command
|
||||
|
||||
@@ -12,16 +12,18 @@ import type {
|
||||
FieldDef,
|
||||
} from 'contentlayer/source-files';
|
||||
import { defineDocumentType } from 'contentlayer/source-files';
|
||||
import type { Options as ImgSizeOptions } from 'rehype-img-size';
|
||||
import type { MDXOptions } from 'contentlayer/core';
|
||||
import type { PluggableList } from 'unified';
|
||||
import type { PluggableList, Plugin } from 'unified';
|
||||
import rehypeImgSize, {
|
||||
type Options as RehypeImgSizeOptions,
|
||||
} from 'rehype-img-size';
|
||||
import type { Root } from 'hast';
|
||||
import {
|
||||
rehypeImgSize,
|
||||
rehypeNextDocs,
|
||||
remarkGfm,
|
||||
structure,
|
||||
type RehypeNextDocsOptions,
|
||||
} from '@/mdx-plugins';
|
||||
import type { RehypeNextDocsOptions } from '@/mdx-plugins/rehype-next-docs';
|
||||
import { getTableOfContents } from '@/server/get-toc';
|
||||
import type { DocsPageBase } from './types';
|
||||
|
||||
@@ -79,11 +81,10 @@ export function create(options: Options = {}): Config {
|
||||
const rehypePlugins: PluggableList = [
|
||||
[rehypeNextDocs, pluginOptions],
|
||||
[
|
||||
// @ts-expect-error -- invalid options type
|
||||
rehypeImgSize,
|
||||
rehypeImgSize as Plugin<[RehypeImgSizeOptions], Root>,
|
||||
{
|
||||
dir: imgDirPath,
|
||||
} satisfies ImgSizeOptions,
|
||||
},
|
||||
],
|
||||
...(mdx.rehypePlugins ?? []),
|
||||
];
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
export {
|
||||
default as rehypeImgSize,
|
||||
type Options as RehypeImgSizeOptions,
|
||||
} from 'rehype-img-size';
|
||||
export {
|
||||
default as remarkGfm,
|
||||
type Options as RemarkGfmOptions,
|
||||
} from 'remark-gfm';
|
||||
export * from './remark-image';
|
||||
export * from './rehype-next-docs';
|
||||
export * from './remark-dynamic-content';
|
||||
export * from './remark-structure';
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { Image, Root, RootContent } from 'mdast';
|
||||
import type { Transformer } from 'unified';
|
||||
import slash from '@/utils/slash';
|
||||
import { visit } from './unist-visit';
|
||||
|
||||
const VALID_BLUR_EXT = ['.jpeg', '.png', '.webp', '.avif', '.jpg'];
|
||||
const EXTERNAL_URL_REGEX = /^https?:\/\//;
|
||||
const PUBLIC_DIR = path.join(process.cwd(), 'public');
|
||||
|
||||
export interface RemarkImageOptions {
|
||||
/**
|
||||
* Preferred placeholder type
|
||||
*
|
||||
* @defaultValue 'blur'
|
||||
*/
|
||||
placeholder?: 'blur' | 'none';
|
||||
}
|
||||
|
||||
// Based on the Nextra: https://github.com/shuding/nextra
|
||||
|
||||
/**
|
||||
* Turn images into static imports
|
||||
*/
|
||||
export function remarkImage({
|
||||
placeholder = 'blur',
|
||||
}: RemarkImageOptions = {}): Transformer<Root, Root> {
|
||||
return (tree, _file, done) => {
|
||||
const importsToInject: { variableName: string; importPath: string }[] = [];
|
||||
|
||||
visit(tree, ['image'], (node: Image) => {
|
||||
let url = decodeURI(node.url);
|
||||
|
||||
if (!url || EXTERNAL_URL_REGEX.test(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.startsWith('/')) {
|
||||
const urlPath = path.join(PUBLIC_DIR, url);
|
||||
|
||||
if (!existsSync(urlPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
url = slash(urlPath);
|
||||
}
|
||||
|
||||
// Unique variable name for the given static image URL
|
||||
const variableName = `__img${importsToInject.length}`;
|
||||
const hasBlur =
|
||||
placeholder === 'blur' &&
|
||||
VALID_BLUR_EXT.some((ext) => url.endsWith(ext));
|
||||
importsToInject.push({ variableName, importPath: url });
|
||||
// Replace the image node with an MDX component node (Next.js Image)
|
||||
Object.assign(node, {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'img',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'alt',
|
||||
value: node.alt ?? 'image',
|
||||
},
|
||||
hasBlur && {
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'placeholder',
|
||||
value: 'blur',
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'src',
|
||||
value: {
|
||||
type: 'mdxJsxAttributeValueExpression',
|
||||
value: variableName,
|
||||
data: {
|
||||
estree: {
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: { type: 'Identifier', name: variableName },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
].filter(Boolean),
|
||||
});
|
||||
});
|
||||
|
||||
if (importsToInject.length) {
|
||||
const imports = importsToInject.map(
|
||||
({ variableName, importPath }) =>
|
||||
({
|
||||
type: 'mdxjsEsm',
|
||||
data: {
|
||||
estree: {
|
||||
body: [
|
||||
{
|
||||
type: 'ImportDeclaration',
|
||||
source: { type: 'Literal', value: importPath },
|
||||
specifiers: [
|
||||
{
|
||||
type: 'ImportDefaultSpecifier',
|
||||
local: { type: 'Identifier', name: variableName },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}) as RootContent,
|
||||
);
|
||||
|
||||
tree.children.unshift(...imports);
|
||||
}
|
||||
|
||||
done();
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { parse } from 'node:path';
|
||||
import slash from '@/utils/slash';
|
||||
import type { FileInfo } from './types';
|
||||
|
||||
export function parseFilePath(path: string): FileInfo {
|
||||
const parsed = parse(path);
|
||||
const dir = parsed.dir.replace('\\', '/');
|
||||
const dir = slash(parsed.dir);
|
||||
const flattenedPath = joinPaths([dir, parsed.name]);
|
||||
const [name, locale] = parsed.name.split('.');
|
||||
|
||||
@@ -49,7 +50,7 @@ export function splitPath(path: string): string[] {
|
||||
/**
|
||||
* Convert paths to an array, slashes within the path will be ignored
|
||||
* @param paths - Paths to join
|
||||
* @param slash - whether to add a trailing/leading slash to path
|
||||
* @param slashMode - whether to add a trailing/leading slash to path
|
||||
* @example
|
||||
* ```
|
||||
* ['a','b','c'] // 'a/b/c'
|
||||
@@ -60,14 +61,14 @@ export function splitPath(path: string): string[] {
|
||||
*/
|
||||
export function joinPaths(
|
||||
paths: string[],
|
||||
slash: 'leading' | 'trailing' | 'none' = 'none',
|
||||
slashMode: 'leading' | 'trailing' | 'none' = 'none',
|
||||
): string {
|
||||
const joined = paths
|
||||
// avoid slashes in path and filter empty
|
||||
.flatMap((path) => splitPath(path))
|
||||
.join('/');
|
||||
|
||||
switch (slash) {
|
||||
switch (slashMode) {
|
||||
case 'leading':
|
||||
return `/${joined}`;
|
||||
case 'trailing':
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export default function slash(path: string): string {
|
||||
const isExtendedLengthPath = path.startsWith('\\\\?\\');
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replace('\\', '/');
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
remarkStructure,
|
||||
remarkHeading,
|
||||
type RehypeNextDocsOptions,
|
||||
remarkImage,
|
||||
type RemarkImageOptions,
|
||||
} from 'fumadocs-core/mdx-plugins';
|
||||
import type { PluggableList } from 'unified';
|
||||
import type { Configuration } from 'webpack';
|
||||
@@ -26,6 +28,8 @@ type MDXOptions = Omit<
|
||||
*/
|
||||
valueToExport?: string[];
|
||||
|
||||
remarkImageOptions?: RemarkImageOptions;
|
||||
|
||||
/**
|
||||
* built-in `fumadocs-core` rehype plugin options
|
||||
*/
|
||||
@@ -88,6 +92,7 @@ const createNextDocs =
|
||||
(v) => [
|
||||
remarkGfm,
|
||||
remarkHeading,
|
||||
[remarkImage, mdxOptions.remarkImageOptions],
|
||||
...v,
|
||||
remarkStructure,
|
||||
[remarkMdxExport, { values: valueToExport }],
|
||||
|
||||
Reference in New Issue
Block a user