@docubook/flame 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docu/components/Context.tsx +2 -2
- package/.docu/components/Pagination.tsx +3 -2
- package/.docu/components/Sublink.tsx +3 -3
- package/.docu/node/html.ts +1 -1
- package/.docu/node/mdx.ts +95 -2
- package/.docu/node/search-indexer.ts +2 -2
- package/.docu/node/server.ts +5 -2
- package/.docu/node/types.ts +3 -3
- package/.docu/node/utils.ts +14 -0
- package/.docu/pages/index.tsx +26 -6
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { cn } from "../node/utils";
|
|
4
|
+
import { cn, docsHtmlHref } from "../node/utils";
|
|
5
5
|
import { Dropdown, DropdownItem } from "@docubook/ui-react/dropdown";
|
|
6
6
|
import { routes, config as docuConfig } from "../node/client-routes";
|
|
7
7
|
import { ChevronsUpDown, Check } from "lucide-react";
|
|
@@ -68,7 +68,7 @@ export function Context({ className }: ContextProps) {
|
|
|
68
68
|
{contextRoutes.map((route) => {
|
|
69
69
|
const isActive = activeRoute?.href === route.href;
|
|
70
70
|
const firstItemPath = getFirstItemHref(route);
|
|
71
|
-
const contextPath = `/docs${firstItemPath}
|
|
71
|
+
const contextPath = docsHtmlHref(`/docs${firstItemPath}`);
|
|
72
72
|
|
|
73
73
|
return (
|
|
74
74
|
<DropdownItem
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { getPreviousNext } from "../node/route";
|
|
5
|
+
import { docsHtmlHref } from "../node/utils";
|
|
5
6
|
import { PaginationDocs } from "@docubook/ui-react/pagination";
|
|
6
7
|
|
|
7
8
|
interface PaginationProps {
|
|
@@ -27,8 +28,8 @@ export default function Pagination({
|
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
30
|
<PaginationDocs
|
|
30
|
-
prev={prev ? { href: `/docs${prev.href}
|
|
31
|
-
next={next ? { href: `/docs${next.href}
|
|
31
|
+
prev={prev ? { href: docsHtmlHref(`/docs${prev.href}`), title: prev.title } : undefined}
|
|
32
|
+
next={next ? { href: docsHtmlHref(`/docs${next.href}`), title: next.title } : undefined}
|
|
32
33
|
className={className}
|
|
33
34
|
prevIcon={prevIcon}
|
|
34
35
|
nextIcon={nextIcon}
|
|
@@ -4,7 +4,7 @@ import { useState, useRef, useEffect } from "react";
|
|
|
4
4
|
import { ChevronDown } from "lucide-react";
|
|
5
5
|
import Anchor from "./Anchor";
|
|
6
6
|
import type { DocuRoute } from "../node/types";
|
|
7
|
-
import { cn } from "../node/utils";
|
|
7
|
+
import { cn, docsHtmlHref } from "../node/utils";
|
|
8
8
|
|
|
9
9
|
interface SublinkProps extends DocuRoute {
|
|
10
10
|
level: number;
|
|
@@ -48,7 +48,7 @@ export default function Sublink({
|
|
|
48
48
|
if (!items) {
|
|
49
49
|
const link = (
|
|
50
50
|
<Anchor
|
|
51
|
-
href={fullHref}
|
|
51
|
+
href={docsHtmlHref(fullHref)}
|
|
52
52
|
className="text-foreground hover:text-foreground/80 text-sm transition-colors"
|
|
53
53
|
activeClassName="text-primary font-medium"
|
|
54
54
|
activeWhen={(path) => path === fullHref || path === `${fullHref}.html`}
|
|
@@ -91,7 +91,7 @@ export default function Sublink({
|
|
|
91
91
|
<span>{title}</span>
|
|
92
92
|
) : (
|
|
93
93
|
<Anchor
|
|
94
|
-
href={fullHref}
|
|
94
|
+
href={docsHtmlHref(fullHref)}
|
|
95
95
|
className="text-foreground hover:text-foreground/80 transition-colors"
|
|
96
96
|
activeClassName="text-primary"
|
|
97
97
|
activeWhen={(path) => path === fullHref || path === `${fullHref}.html`}
|
package/.docu/node/html.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
59
59
|
</head>
|
|
60
60
|
<body>
|
|
61
61
|
<div id="root">${body}</div>
|
|
62
|
-
<script${nonceAttr} src="${Bun.escapeHTML(assetPrefix + js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
|
|
62
|
+
<script type="module"${nonceAttr} src="${Bun.escapeHTML(assetPrefix + js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
|
|
63
63
|
</body>
|
|
64
64
|
</html>`;
|
|
65
65
|
}
|
package/.docu/node/mdx.ts
CHANGED
|
@@ -11,6 +11,96 @@ import {
|
|
|
11
11
|
import { createMdxComponents } from "@docubook/mdx-content";
|
|
12
12
|
import { getGitLastModified, getGitLastModifiedBatch } from "./utils";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Return the value with `.html` appended, or null if the value should be left
|
|
16
|
+
* unchanged. Rules:
|
|
17
|
+
* - Must be a string
|
|
18
|
+
* - Must start with /docs/ (the /docs root index needs no suffix)
|
|
19
|
+
* - Must not be an external URL, contain a fragment, or already end in .html
|
|
20
|
+
*/
|
|
21
|
+
function appendHtml(value: unknown): string | null {
|
|
22
|
+
if (typeof value !== "string") return null;
|
|
23
|
+
if (/^https?:\/\//.test(value)) return null;
|
|
24
|
+
if (!value.startsWith("/docs/")) return null;
|
|
25
|
+
if (value.includes("#")) return null;
|
|
26
|
+
if (value.endsWith(".html")) return null;
|
|
27
|
+
return `${value}.html`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface HastNode {
|
|
31
|
+
type: string;
|
|
32
|
+
tagName?: string;
|
|
33
|
+
properties?: Record<string, unknown>;
|
|
34
|
+
children?: HastNode[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MdastNode {
|
|
38
|
+
type: string;
|
|
39
|
+
// MDX JSX nodes carry their props as an attributes array
|
|
40
|
+
attributes?: { type: string; name: string; value: unknown }[];
|
|
41
|
+
children?: MdastNode[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Rehype plugin: append `.html` to internal `/docs/` hrefs on HTML `<a>` nodes.
|
|
46
|
+
*
|
|
47
|
+
* This covers standard markdown links: `[text](/docs/page)` → `<a href="…">`.
|
|
48
|
+
* It runs in the HAST (HTML AST) phase, where `<a>` elements are real nodes.
|
|
49
|
+
*
|
|
50
|
+
* Skips: external URLs, anchor-only links, paths that already end in `.html`,
|
|
51
|
+
* and the `/docs` root index (no trailing slash segment).
|
|
52
|
+
*/
|
|
53
|
+
function rehypeDocsHtmlLinks() {
|
|
54
|
+
return (tree: HastNode) => {
|
|
55
|
+
function walk(node: HastNode): void {
|
|
56
|
+
if (node.type === "element" && node.tagName === "a") {
|
|
57
|
+
const fixed = appendHtml(node.properties?.href);
|
|
58
|
+
if (fixed) node.properties!.href = fixed;
|
|
59
|
+
}
|
|
60
|
+
if (node.children) {
|
|
61
|
+
for (const child of node.children) walk(child);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
walk(tree);
|
|
65
|
+
return tree;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Remark plugin: append `.html` to internal `/docs/` hrefs on MDX JSX nodes.
|
|
71
|
+
*
|
|
72
|
+
* MDX JSX elements (`<Card href="…">`, `<LinkCard href="…">`, etc.) live in
|
|
73
|
+
* the MDAST as `mdxJsxFlowElement` / `mdxJsxTextElement` nodes. They are
|
|
74
|
+
* compiled directly to JavaScript by the MDX compiler *before* rehype runs,
|
|
75
|
+
* so a rehype plugin can never see them as `<a>` elements. This remark plugin
|
|
76
|
+
* intercepts them at the MDAST phase where their `attributes` array is still
|
|
77
|
+
* accessible and mutable.
|
|
78
|
+
*
|
|
79
|
+
* Skips: same rules as `appendHtml` (external URLs, anchors, already `.html`).
|
|
80
|
+
*/
|
|
81
|
+
function remarkMdxJsxDocsHtmlLinks() {
|
|
82
|
+
return (tree: MdastNode) => {
|
|
83
|
+
function walk(node: MdastNode): void {
|
|
84
|
+
if (
|
|
85
|
+
(node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") &&
|
|
86
|
+
node.attributes
|
|
87
|
+
) {
|
|
88
|
+
for (const attr of node.attributes) {
|
|
89
|
+
if (attr.type === "mdxJsxAttribute" && attr.name === "href") {
|
|
90
|
+
const fixed = appendHtml(attr.value);
|
|
91
|
+
if (fixed) attr.value = fixed;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (node.children) {
|
|
96
|
+
for (const child of node.children) walk(child);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
walk(tree);
|
|
100
|
+
return tree;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
14
104
|
export { getGitLastModifiedBatch };
|
|
15
105
|
|
|
16
106
|
export interface MdxResult {
|
|
@@ -46,8 +136,11 @@ export async function compileMdx(
|
|
|
46
136
|
const defaultRemark = createDefaultRemarkPlugins();
|
|
47
137
|
const defaultRehype = createDefaultRehypePlugins();
|
|
48
138
|
|
|
49
|
-
|
|
50
|
-
|
|
139
|
+
// remarkMdxJsxDocsHtmlLinks must run before user plugins so custom remark
|
|
140
|
+
// transforms see already-fixed hrefs. rehypeDocsHtmlLinks handles plain
|
|
141
|
+
// markdown [text](path) → <a> elements in the HAST phase.
|
|
142
|
+
const finalRemark = [...defaultRemark, remarkMdxJsxDocsHtmlLinks, ...(remarkPlugins ?? [])];
|
|
143
|
+
const finalRehype = [...defaultRehype, rehypeDocsHtmlLinks, ...(rehypePlugins ?? [])];
|
|
51
144
|
|
|
52
145
|
const serialized = await serialize(strippedContent, {
|
|
53
146
|
mdxOptions: {
|
|
@@ -15,7 +15,7 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
|
15
15
|
import { resolve, join } from "node:path";
|
|
16
16
|
import { extractFrontmatterWithContent } from "@docubook/core";
|
|
17
17
|
import { DOCS_DIR, ASSETS_DIR, loadDocuConfig } from "./paths";
|
|
18
|
-
import { scanMdxFiles } from "./utils";
|
|
18
|
+
import { scanMdxFiles, docsHtmlHref } from "./utils";
|
|
19
19
|
|
|
20
20
|
const docuConfig = loadDocuConfig();
|
|
21
21
|
|
|
@@ -72,7 +72,7 @@ export function stripJsx(content: string): string {
|
|
|
72
72
|
export function extractRecords(filePath: string, raw: string): SearchRecord[] {
|
|
73
73
|
const { frontmatter, strippedContent: content } = extractFrontmatterWithContent<Frontmatter>(raw);
|
|
74
74
|
const records: SearchRecord[] = [];
|
|
75
|
-
const url = `/docs/${filePath}
|
|
75
|
+
const url = docsHtmlHref(`/docs/${filePath}`);
|
|
76
76
|
const lvl0 = getSectionTitle(filePath);
|
|
77
77
|
const lvl1 = frontmatter.title || null;
|
|
78
78
|
|
package/.docu/node/server.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type ServerState,
|
|
17
17
|
} from "./server-routes";
|
|
18
18
|
import { wrapPluginResponse } from "./security";
|
|
19
|
+
import { stripDocsHtmlSuffix } from "./utils";
|
|
19
20
|
|
|
20
21
|
const docuConfig = loadDocuConfig();
|
|
21
22
|
|
|
@@ -103,7 +104,9 @@ const server = Bun.serve({
|
|
|
103
104
|
|
|
104
105
|
async fetch(req) {
|
|
105
106
|
const url = new URL(req.url);
|
|
106
|
-
|
|
107
|
+
// Generated links carry `.html` (matching the static build output);
|
|
108
|
+
// route them to the same handler as their extensionless form.
|
|
109
|
+
const pathname = stripDocsHtmlSuffix(url.pathname);
|
|
107
110
|
const startTime = performance.now();
|
|
108
111
|
|
|
109
112
|
if (builder) {
|
|
@@ -150,7 +153,7 @@ const server = Bun.serve({
|
|
|
150
153
|
if (staticRes) return staticRes;
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
const match = router?.match(
|
|
156
|
+
const match = router?.match(pathname);
|
|
154
157
|
let response: Response;
|
|
155
158
|
|
|
156
159
|
if (match) {
|
package/.docu/node/types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ThemeConfig } from "@docubook/themes-colors";
|
|
2
|
+
import type { PluginEntry } from "./plugin";
|
|
3
|
+
|
|
1
4
|
export interface DocuRouteContext {
|
|
2
5
|
title?: string;
|
|
3
6
|
icon?: string;
|
|
@@ -73,9 +76,6 @@ export interface HomeConfig {
|
|
|
73
76
|
features?: HomeFeature[];
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
import type { ThemeConfig } from "@docubook/themes-colors";
|
|
77
|
-
import type { PluginEntry } from "./plugin";
|
|
78
|
-
|
|
79
79
|
export interface DocuConfig {
|
|
80
80
|
meta: DocuMeta;
|
|
81
81
|
home?: HomeConfig;
|
package/.docu/node/utils.ts
CHANGED
|
@@ -44,6 +44,20 @@ export function isExternalUrl(url: string): boolean {
|
|
|
44
44
|
return /^(https?:\/\/|\/\/)/.test(url);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Suffix an internal docs link with `.html` to match the flat static build output. */
|
|
48
|
+
export function docsHtmlHref(path: string): string {
|
|
49
|
+
return `${path}.html`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Map a `/docs{.html,/*.html}` request back to its extensionless route (dev server).
|
|
53
|
+
* Handles both `/docs/*.html` (pages) and `/docs.html` (edge case). */
|
|
54
|
+
export function stripDocsHtmlSuffix(pathname: string): string {
|
|
55
|
+
if (pathname === "/docs.html") return "/docs";
|
|
56
|
+
if (pathname.startsWith("/docs/") && pathname.endsWith(".html"))
|
|
57
|
+
return pathname.slice(0, -".html".length);
|
|
58
|
+
return pathname;
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
export function getPath(url: string): string {
|
|
48
62
|
try {
|
|
49
63
|
return new URL(url).pathname;
|
package/.docu/pages/index.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadDocuConfig } from "../node/paths";
|
|
2
|
+
import { docsHtmlHref, isExternalUrl } from "../node/utils";
|
|
2
3
|
import { Hero, Features } from "../components/home";
|
|
3
4
|
import type { HomeFeature } from "../node/types";
|
|
4
5
|
|
|
@@ -21,23 +22,42 @@ export default function IndexPage() {
|
|
|
21
22
|
const { meta, home } = docuConfig;
|
|
22
23
|
const routes = (docuConfig.routes as RouteItem[]) || [];
|
|
23
24
|
|
|
25
|
+
// Docs pages under /docs/ are flat .html files; the root /docs is
|
|
26
|
+
// docs/index.html via directory index — no .html suffix needed.
|
|
27
|
+
const linkWithHtml = (link: string) => {
|
|
28
|
+
if (isExternalUrl(link)) return link;
|
|
29
|
+
if (link.startsWith("/docs/")) return `${link}.html`;
|
|
30
|
+
return link;
|
|
31
|
+
};
|
|
32
|
+
|
|
24
33
|
// Use home.features if configured, otherwise fallback to routes with context
|
|
25
34
|
const features: HomeFeature[] =
|
|
26
|
-
home?.features
|
|
35
|
+
home?.features?.map((f) => ({
|
|
36
|
+
...f,
|
|
37
|
+
link: f.link ? linkWithHtml(f.link) : undefined,
|
|
38
|
+
})) ||
|
|
27
39
|
routes
|
|
28
40
|
.filter((r) => r.context)
|
|
29
41
|
.map((route) => ({
|
|
30
42
|
icon: route.context?.icon,
|
|
31
43
|
title: route.context?.title || route.title,
|
|
32
44
|
description: route.context?.description || "",
|
|
33
|
-
link: `/docs${route.href}${route.items?.[0]?.href || ""}
|
|
45
|
+
link: docsHtmlHref(`/docs${route.href}${route.items?.[0]?.href || ""}`),
|
|
34
46
|
}));
|
|
35
47
|
|
|
36
48
|
// Use home.hero if configured, otherwise fallback to meta
|
|
37
|
-
const hero = home?.hero
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
const hero = home?.hero
|
|
50
|
+
? {
|
|
51
|
+
...home.hero,
|
|
52
|
+
actions: home.hero.actions?.map((a) => ({
|
|
53
|
+
...a,
|
|
54
|
+
link: linkWithHtml(a.link),
|
|
55
|
+
})),
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
headline: meta.title,
|
|
59
|
+
description: meta.description,
|
|
60
|
+
};
|
|
41
61
|
|
|
42
62
|
return (
|
|
43
63
|
<div className="bg-base-100 relative isolate min-h-screen overflow-hidden">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docubook/flame",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"react": "^19.2.7",
|
|
53
53
|
"react-dom": "^19.2.7",
|
|
54
54
|
"unified": "^11.0.0",
|
|
55
|
-
"@docubook/core": "^1.8.0",
|
|
56
|
-
"@docubook/mdx-content": "^3.4.0",
|
|
57
55
|
"@docubook/ui-react": "^0.1.4",
|
|
56
|
+
"@docubook/mdx-content": "^3.4.1",
|
|
57
|
+
"@docubook/core": "^1.8.0",
|
|
58
58
|
"@docubook/themes-colors": "^0.10.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|