Skip to content

Commit 5e8fb20

Browse files
committed
Improve MDX parsing, local source, and repo config
Enhanced MDX parsing to clean TOC titles and better handle type annotations and style attributes. Improved local source handling for meta JSON files and added support for writing meta files. Updated repository config to include LICENSE.md and docsPath, and adjusted scripts to set NEXT_DOC_SOURCE for static builds. Also improved relative link resolution for local sources.
1 parent a290304 commit 5e8fb20

10 files changed

Lines changed: 192 additions & 48 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# test & build
1313
/coverage
1414
/fi-plugins/
15+
/fi-plugins*/
1516
/.next/
1617
/out/
1718
/build

components/mdx.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock";
1414
import type { MDXComponents } from "mdx/types";
1515
import { siteConfig } from "@/lib/site-config";
1616
import { RepositoryConfig, VersionConfig } from "@/lib/repo-config";
17+
import { source, isLocal } from "@/lib/source";
1718

1819
const githubCalloutRegex =
1920
/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*(.*)$/s;
@@ -247,5 +248,26 @@ export function createRelativeLink(
247248
let repositoryUrl = `${repository.repository_url}/blob/${version.github_branch || version.version}/${href}`;
248249
// console.debug("### Creating relative link for:", href, repositoryUrl);
249250

251+
if (isLocal) {
252+
// 1. limited_files take precedence
253+
let localPage = source.getPage([repository.repo, version.version, href.replace(/\.mdx?$/, "").toLowerCase()])
254+
if (!localPage &&repository.docsPath) {
255+
// 2. search in the docsPath
256+
// (replace leading 'repository.docsPath' if any)
257+
localPage = source.getPage([repository.repo, version.version, href.replace(new RegExp(`^${repository.docsPath}/?`), "").replace(/\.mdx?$/, "").toLowerCase()])
258+
}
259+
260+
if (localPage) {
261+
console.debug("### Found local page for link:", href, "->", localPage.url);
262+
// Convert to internal docs link
263+
repositoryUrl = `${siteConfig.baseUrl}/${localPage.url.replace(/^\//, "")}`;
264+
}
265+
}
266+
267+
// Converting relative links to absolute links for Vercel's preview image generation
268+
if (repositoryUrl.startsWith("/")) {
269+
repositoryUrl = siteConfig.baseUrl + repositoryUrl;
270+
}
271+
250272
return repositoryUrl;
251273
}

lib/compile-md.ts

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,56 @@ export interface CompiledPage {
2020
body: FC<{ components?: MDXComponents }>;
2121
}
2222

23+
// Function to clean TOC items and convert them to plain text
24+
function cleanTocItems(toc: TableOfContents): TableOfContents {
25+
return toc.map(item => ({
26+
...item,
27+
title: cleanTocTitle(item.title)
28+
}));
29+
}
30+
31+
// Function to convert TOC title to plain text
32+
function cleanTocTitle(title: any): string {
33+
if (typeof title === 'string') {
34+
// Remove HTML tags and decode entities
35+
return title
36+
.replace(/<[^>]*>/g, '') // Remove HTML tags
37+
.replace(/&lt;/g, '<')
38+
.replace(/&gt;/g, '>')
39+
.replace(/&amp;/g, '&')
40+
.replace(/&quot;/g, '"')
41+
.replace(/&#39;/g, "'")
42+
.trim();
43+
}
44+
45+
if (title && typeof title === 'object') {
46+
// Handle React nodes - extract text content
47+
if (title.props && title.props.children) {
48+
return cleanTocTitle(title.props.children);
49+
}
50+
51+
if (Array.isArray(title)) {
52+
return title.map(cleanTocTitle).filter(Boolean).join('');
53+
}
54+
55+
// If it's a React element with type and props
56+
if (title.type === 'a' && title.props && title.props.children) {
57+
return cleanTocTitle(title.props.children);
58+
}
59+
60+
// Handle other React node types
61+
if (title.toString && typeof title.toString === 'function') {
62+
return cleanTocTitle(title.toString());
63+
}
64+
}
65+
66+
if (Array.isArray(title)) {
67+
return title.map(cleanTocTitle).filter(Boolean).join('');
68+
}
69+
70+
return String(title || '').trim();
71+
}
72+
2373
// Create a cached compile function using React cache
2474
const cachedCompile = reactCache(async (filePath: string, source: string): Promise<CompiledPage> => {
2575
// console.time(`compile md: ${filePath}`);
@@ -31,7 +81,7 @@ const cachedCompile = reactCache(async (filePath: string, source: string): Promi
3181
})
3282
.then((compiled) => ({
3383
body: compiled.body,
34-
toc: compiled.toc,
84+
toc: cleanTocItems(compiled.toc), // Clean TOC items to ensure plain text titles
3585
...compiled.frontmatter,
3686
}))
3787
.catch((error) => {
@@ -130,11 +180,51 @@ function parseSourceBeforeCompile(filePath: string, source: string): string {
130180
'<div class="note">$1</div>',
131181
);
132182

133-
// Replace "style="xx" as mdx format
183+
// Fix type annotations that get interpreted as HTML tags
184+
// Replace array<type> with array&lt;type&gt; to prevent HTML parsing
134185
parsedSource = parsedSource.replace(
135-
/style="([^"]+)"/g,
136-
(match, p1) => {
137-
return `style={{ ${p1} }}`;
186+
/array<([^>]+)>/g,
187+
'array&lt;$1&gt;',
188+
);
189+
190+
// Fix standalone type annotations at start of lines or after colons/spaces
191+
// This handles cases like "- `id`: int" -> "- `id`: `int`"
192+
parsedSource = parsedSource.replace(
193+
/(\s+- `[^`]+`):\s+(int|string|bool|float|double|object|array)(\s|$)/g,
194+
'$1: `$2`$3',
195+
);
196+
197+
// Fix type annotations in parameter lists
198+
// This handles cases like "- **tags**: array<int> (optional)"
199+
parsedSource = parsedSource.replace(
200+
/(\*\*[^*]+\*\*):\s+(array&lt;[^&]+&gt;|int|string|bool|float|double|object|array)(\s)/g,
201+
'$1: `$2`$3',
202+
);
203+
204+
// Replace "style="xx" as mdx format
205+
// Convert CSS style attributes to JSX format
206+
parsedSource = parsedSource.replace(
207+
/style\s*=\s*"([^"]+)"/g,
208+
(match: string, styleValue: string): string => {
209+
// Parse CSS properties and convert to JSX object
210+
const styles = styleValue
211+
.split(';')
212+
.filter((prop: string) => prop.trim())
213+
.map((prop: string) => {
214+
const [property, ...valueParts] = prop.split(':');
215+
const value = valueParts.join(':').trim(); // Handle values with colons
216+
const trimmedProperty = property.trim();
217+
218+
if (!trimmedProperty || !value) return '';
219+
220+
// Convert kebab-case to camelCase
221+
const camelProperty = trimmedProperty.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
222+
return `${camelProperty}: "${value}"`;
223+
})
224+
.filter((prop: string) => prop)
225+
.join(', ');
226+
227+
return `style={{ ${styles} }}`;
138228
}
139229
);
140230

@@ -144,9 +234,10 @@ function parseSourceBeforeCompile(filePath: string, source: string): string {
144234
'\\{\\{$1\\}\\}',
145235
);
146236

237+
147238
return parsedSource;
148239
}
149240

150241
export async function compile(filePath: string, source: string) {
151242
return cachedCompile(filePath, source);
152-
}
243+
}

lib/meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const fumadocMeta: VirtualFile[] = [
8787
root: false,
8888
pages: [
8989
`./index`, // e.g. /docs/repo-slug/3.x/
90+
`./overview`, // e.g. /docs/repo-slug/3.x/overview
9091
"...", // all other files in this version
9192
],
9293
},

lib/repo-config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ const rawRepositories = [
4040
"https://github.com/solutionforest/filament-cms-website-plugin",
4141
latest_version: "3.x",
4242
is_private: true,
43+
docsPath: "docs", // include docs
4344
versions: [
4445
{
4546
version: "1.x",
4647
github_branch: "1.x",
4748
limited_files: [
4849
{ name: "README.md", title: "Overview", slug: "overview" },
4950
{ name: "Documentation.md", title: "Documentation", slug: "docs" },
51+
{ name: "LICENSE.md", title: "License", slug: "license" },
5052
],
5153
},
5254
{
@@ -55,14 +57,15 @@ const rawRepositories = [
5557
limited_files: [
5658
{ name: "README.md", title: "Overview", slug: "overview" },
5759
{ name: "Documentation.md", title: "Documentation", slug: "docs" },
60+
{ name: "LICENSE.md", title: "License", slug: "license" },
5861
],
5962
},
6063
{
6164
version: "3.x",
6265
github_branch: "3.x",
6366
limited_files: [
6467
{ name: "README.md", title: "Overview", slug: "overview" },
65-
{ name: "Documentation.md", title: "Documentation", slug: "docs" },
68+
{ name: "LICENSE.md", title: "License", slug: "license" },
6669
],
6770
},
6871
],
@@ -71,7 +74,7 @@ const rawRepositories = [
7174
// description: 'Effortlessly manage your newsletters with our Filament Newsletter package, designed for seamless integration with the Filament Admin Panel. Enjoy flexible and user-friendly email marketing directly within your admin panel.',
7275
// repository_url: 'https://github.com/solutionforest/filaletter',
7376
// latest_version: '3.x',
74-
// is_private: true,
77+
// is_private: false,
7578
// versions: [
7679
// {
7780
// version: '2.x',

lib/source.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ const FileNameRegex = /^\d\d-(.+)$/;
88
export const isLocal =
99
process.env.NEXT_DOC_SOURCE === 'local'; // || process.env.NEXT_PHASE === "phase-production-build";
1010

11-
console.debug('#### Is Local: ', isLocal);
12-
1311
export const source = loader({
1412
baseUrl: "/docs",
1513
// source: await createLocalSource(),
@@ -19,16 +17,12 @@ export const source = loader({
1917
try {
2018
const segments = info.path
2119
.split("/")
22-
.filter((seg) => !(seg.startsWith("(") && seg.endsWith(")")))
23-
.map((seg) => {
24-
const res = FileNameRegex.exec(seg);
25-
26-
return res ? res[1] : seg;
27-
});
20+
.filter((seg) => !(seg.startsWith("(") && seg.endsWith(")")));
2821

2922
if (segments.at(-1) === "index") {
3023
segments.pop();
3124
}
25+
// console.debug("Generating slugs for:", info.path, "->", segments);
3226

3327
return segments;
3428
} catch (error) {

lib/sources/github.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
type VersionConfig,
1212
} from "../repo-config";
1313
import { cache as reactCache } from 'react';
14-
import { buildMarkdownFileToLocal } from "./local";
14+
import { buildMarkdownFileToLocal, buildMetaJson } from "./local";
1515

1616
const token = process.env.GITHUB_TOKEN;
1717
if (!token) throw new Error(`environment variable GITHUB_TOKEN is needed.`);
@@ -283,6 +283,7 @@ async function fetchRepositoryFiles(
283283
const docsSha = config.docsPath
284284
? await getDirectorySha(config, version, config.docsPath)
285285
: null;
286+
console.debug(`Docs SHA for ${config.owner}/${config.repo} (${version.version}) [${config.docsPath}]:`, docsSha);
286287
if (docsSha) {
287288
try {
288289
// Use cached GitHub tree function for better performance
@@ -317,8 +318,11 @@ async function fetchRepositoryFiles(
317318
// Parsing file path
318319
const parsedFilePath = path.parse(file.path);
319320
const pageTitle = getTitleFromFile(parsedFilePath.name);
321+
const cleanFileRelativePath = parsedFilePath.dir.length > 0
322+
? `${parsedFilePath.dir}/${parsedFilePath.name}`
323+
: parsedFilePath.name;
320324

321-
const pagePath = `${repoSlug}/${versionSlug}/${parsedFilePath.dir}/${parsedFilePath.name}`;
325+
const pagePath = `${repoSlug}/${versionSlug}/${cleanFileRelativePath}`;
322326

323327
if (!docFolderPages.has(parsedFilePath.dir)) {
324328
docFolderPages.set(parsedFilePath.dir, []);
@@ -327,7 +331,7 @@ async function fetchRepositoryFiles(
327331

328332
// console.debug(`Docs file found: ${file.path} (Parsed: ${formattedFilePath})`);
329333

330-
// console.debug(`2| Adding doc file: ${pagePath} (title: ${pageTitle})`);
334+
console.debug(`2| Adding doc file: ${pagePath} (title: ${pageTitle})`);
331335

332336
const content = await fetchBlob(file.url as string);
333337

@@ -389,6 +393,8 @@ async function fetchRepositoryFiles(
389393

390394
await buildMarkdownFileToLocal(rawMarkdownFiles);
391395

396+
buildMetaJson(allFiles);
397+
392398
return allFiles;
393399
}
394400

@@ -436,4 +442,4 @@ export async function createGitHubSource(): Promise<
436442
return {
437443
files: [...allPages, ...fumadocMeta],
438444
};
439-
}
445+
}

0 commit comments

Comments
 (0)