Core: Respect title in images

This commit is contained in:
Fuma Nama
2025-12-04 10:33:48 +08:00
parent 36648f60c1
commit e0c4c3a5e9
4 changed files with 87 additions and 67 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
[Remark Image] Respect `title` in images
+59 -51
View File
@@ -4,7 +4,7 @@ import type { Transformer } from 'unified';
import { visit } from 'unist-util-visit';
import type { MdxjsEsm } from 'mdast-util-mdxjs-esm';
import type { ISizeCalculationResult } from 'image-size/types/interface';
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
import { fileURLToPath } from 'node:url';
const VALID_BLUR_EXT = ['.jpeg', '.png', '.webp', '.avif', '.jpg'];
@@ -94,6 +94,22 @@ export function remarkImage({
src: Source,
node: Image,
): Promise<MdxJsxFlowElement | undefined> {
const attributes: MdxJsxAttribute[] = [
{
type: 'mdxJsxAttribute',
name: 'alt',
value: node.alt ?? 'image',
},
];
if (node.title) {
attributes.push({
type: 'mdxJsxAttribute',
name: 'title',
value: node.title,
});
}
if (src.type === 'file' && useImport) {
// Unique variable name for the given static image URL
const variableName = `__img${importsToInject.length}`;
@@ -112,37 +128,32 @@ export function remarkImage({
importPath: getImportPath(src.file, file.dirname),
});
attributes.push({
type: 'mdxJsxAttribute',
name: 'src',
value: {
type: 'mdxJsxAttributeValueExpression',
value: variableName,
data: {
estree: {
body: [
{
type: 'ExpressionStatement',
expression: { type: 'Identifier', name: variableName },
},
],
type: 'Program',
sourceType: 'script',
},
},
},
});
const out: MdxJsxFlowElement = {
children: [],
type: 'mdxJsxFlowElement',
name: 'img',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'alt',
value: node.alt ?? 'image',
},
{
type: 'mdxJsxAttribute',
name: 'src',
value: {
type: 'mdxJsxAttributeValueExpression',
value: variableName,
data: {
estree: {
body: [
{
type: 'ExpressionStatement',
expression: { type: 'Identifier', name: variableName },
},
],
type: 'Program',
sourceType: 'script',
},
},
},
},
],
attributes,
};
if (hasBlur) {
@@ -167,32 +178,29 @@ export function remarkImage({
if (!size) return;
attributes.push(
{
type: 'mdxJsxAttribute',
name: 'src',
// `src` doesn't support file paths, we can use `node.url` for files and let the underlying framework handle it
value: src.type === 'url' ? src.url.toString() : node.url,
},
{
type: 'mdxJsxAttribute',
name: 'width',
value: size.width.toString(),
},
{
type: 'mdxJsxAttribute',
name: 'height',
value: size.height.toString(),
},
);
return {
type: 'mdxJsxFlowElement',
name: 'img',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'alt',
value: node.alt ?? 'image',
},
{
type: 'mdxJsxAttribute',
name: 'src',
// `src` doesn't support file paths, we can use `node.url` for files and let the underlying framework handle it
value: src.type === 'url' ? src.url.toString() : node.url,
},
{
type: 'mdxJsxAttribute',
name: 'width',
value: size.width.toString(),
},
{
type: 'mdxJsxAttribute',
name: 'height',
value: size.height.toString(),
},
],
attributes,
children: [],
};
}
+14 -13
View File
@@ -180,12 +180,13 @@ export function createCore(options: CoreOptions) {
if (out) config = out;
}
workspaces = [];
for (const workspace of Object.values(config.workspaces)) {
const core = createCore(options);
await core.init({ config: workspace.config });
workspaces.push(core);
}
workspaces = await Promise.all(
Object.values(config.workspaces).map(async (workspace) => {
const core = createCore(options);
await core.init({ config: workspace.config });
return core;
}),
);
},
getOptions() {
return options;
@@ -221,22 +222,22 @@ export function createCore(options: CoreOptions) {
EmitEntry[]
> {
const ctx = this.getPluginContext();
const files: EmitEntry[] = [];
async function add(v: Awaitable<EmitEntry[] | false>[]) {
const files = new Map<string, EmitEntry>();
async function add(...v: Awaitable<EmitEntry[] | false>[]) {
for (const li of await Promise.all(v)) {
if (li === false) continue;
for (const item of li) files.push(item);
for (const item of li) files.set(item.path, item);
}
}
add(
plugins.map((plugin) => {
await add(
...plugins.map((plugin) => {
if (!filterPlugin(plugin) || !plugin.emit) return false;
return plugin.emit.call(ctx);
}),
...workspaces.map((workspace) => workspace.emit({ filterPlugin })),
);
add(workspaces.map((workspace) => workspace.emit({ filterPlugin })));
return files;
return Array.from(files.values());
},
async emitAndWrite(emitOptions?: EmitOptions): Promise<void> {
const start = performance.now();
+9 -3
View File
@@ -1,4 +1,4 @@
import type { Core, Plugin, PluginContext } from '@/core';
import type { Core, CoreOptions, Plugin, PluginContext } from '@/core';
import type {
LoadedConfig,
CollectionItem,
@@ -255,12 +255,18 @@ async function generateDynamicIndexFile({
serverOptions,
tc,
}: FileGenContext) {
const { configPath } = core.getOptions();
const { configPath, environment, outDir } = core.getOptions();
// serializable config options
const partialOptions: CoreOptions = {
configPath,
environment,
outDir,
};
codegen.lines.push(
`import { dynamic } from 'fumadocs-mdx/runtime/dynamic';`,
`import * as Config from '${codegen.formatImportPath(configPath)}';`,
'',
`const create = await dynamic<typeof Config, ${tc}>(Config, ${JSON.stringify(core.getOptions())}, ${JSON.stringify(serverOptions)});`,
`const create = await dynamic<typeof Config, ${tc}>(Config, ${JSON.stringify(partialOptions)}, ${JSON.stringify(serverOptions)});`,
);
async function generateCollectionObjectEntry(