@docubook/core 1.0.0

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/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @docubook/core
2
+
3
+ Shared MDX compile pipeline and markdown utilities for DocuBook.
4
+
5
+ ## Features
6
+
7
+ - **Centralized MDX Pipeline** - One shared compile flow for all DocuBook projects
8
+ - **Managed Remark/Rehype Plugins** - Core markdown plugins are maintained by DocuBook author
9
+ - **Content-First Workflow** - Users can focus on docs content instead of plugin maintenance
10
+ - **Utility Helpers** - Frontmatter extraction, TOC extraction, and slug helpers included
11
+ - **Consistent Behavior** - Same markdown rendering behavior across apps and templates
12
+
13
+ ## Installation
14
+
15
+ Node.js ecosystem:
16
+
17
+ ```bash
18
+ npm install @docubook/core
19
+ ```
20
+
21
+ ```bash
22
+ pnpm add @docubook/core
23
+ ```
24
+
25
+ ```bash
26
+ yarn add @docubook/core
27
+ ```
28
+
29
+ Bun (independent runtime/package manager):
30
+
31
+ ```bash
32
+ bun add @docubook/core
33
+ ```
34
+
35
+ ## Dependency Management Policy
36
+
37
+ Dependencies required for markdown processing are managed in this package and updated by the DocuBook author.
38
+
39
+ This means app-level users should focus on content and integration. Plugin upgrades, compatibility checks, and pipeline maintenance are handled centrally by DocuBook.
40
+
41
+ ### Managed Markdown Dependencies
42
+
43
+ - gray-matter
44
+ - rehype-autolink-headings
45
+ - rehype-code-titles
46
+ - rehype-prism-plus
47
+ - rehype-slug
48
+ - remark-gfm
49
+ - unist-util-visit
50
+
51
+ The most important part is the `remark` and `rehype` plugin stack, which is intentionally owned by this package to avoid dependency drift across apps.
52
+
53
+ ## Why This Matters
54
+
55
+ - Consistent behavior across all DocuBook-based projects
56
+ - Easier maintenance and safer upgrades
57
+ - Less dependency duplication in app-level package.json files
58
+ - Faster onboarding for users who only need to write docs
59
+
60
+ ## Usage
61
+
62
+ ```ts
63
+ import {
64
+ parseMdx,
65
+ extractFrontmatter,
66
+ extractTocsFromRawMdx,
67
+ } from "@docubook/core";
68
+
69
+ const raw = `---\ntitle: Intro\n---\n\n## Hello`;
70
+
71
+ const frontmatter = extractFrontmatter<{ title: string }>(raw);
72
+ const toc = extractTocsFromRawMdx(raw);
73
+ const compiled = await parseMdx<{ title: string }>(raw);
74
+ ```
75
+
76
+ ### File-Based Pipeline (Recommended)
77
+
78
+ ```ts
79
+ import {
80
+ createMdxContentService,
81
+ readMdxFileBySlug,
82
+ parseMdxFile,
83
+ compileParsedMdxFile,
84
+ } from "@docubook/core";
85
+
86
+ type Frontmatter = {
87
+ title: string;
88
+ description: string;
89
+ image: string;
90
+ date: string;
91
+ };
92
+
93
+ const raw = await readMdxFileBySlug("getting-started/introduction");
94
+ const parsed = parseMdxFile<Frontmatter>(raw);
95
+ const compiled = await compileParsedMdxFile(parsed, {
96
+ components: {
97
+ // your mdx components
98
+ },
99
+ });
100
+
101
+ const docsService = createMdxContentService<Frontmatter>({
102
+ parseOptions: {
103
+ components: {
104
+ // your mdx components
105
+ },
106
+ },
107
+ });
108
+
109
+ const doc = await docsService.getCompiledForSlug("getting-started/introduction");
110
+ ```
111
+
112
+ ## API Overview
113
+
114
+ - `parseMdx` - Compiles raw MDX with default or custom plugin options
115
+ - `createMdxContentService` - Unified high-level API for read/parse/compile/getters
116
+ - `readMdxFileBySlug` - Reads `slug.mdx` or `slug/index.mdx` from docs directory
117
+ - `parseMdxFile` - Converts raw file result into `frontmatter` + `tocs` + `content`
118
+ - `compileParsedMdxFile` - Compiles a parsed document while preserving metadata
119
+ - `extractFrontmatter` - Parses frontmatter from raw markdown/MDX
120
+ - `extractTocsFromRawMdx` - Extracts headings for TOC generation
121
+ - `sluggify` - Converts heading text into URL-friendly slugs
122
+
123
+ ## Notes
124
+
125
+ If your app uses `next-mdx-remote` directly for rendering in custom components, keep that direct dependency in the app.
126
+
127
+ For compile pipeline plugins (especially `remark` and `rehype` plugins), rely on this package and avoid re-declaring them at app level unless you have a specific override requirement.
128
+
129
+ ## License
130
+
131
+ MIT
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@docubook/core",
3
+ "version": "1.0.0",
4
+ "description": "Shared MDX compile pipeline and markdown utilities for DocuBook",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "build": "echo 'core uses source exports'"
12
+ },
13
+ "keywords": [
14
+ "docubook",
15
+ "documentation",
16
+ "docs framework",
17
+ "mdx compile",
18
+ "markdown utilities"
19
+ ],
20
+ "homepage": "https://docubook.pro/",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/DocuBook/docubook"
24
+ },
25
+ "author": "wildan.nrs",
26
+ "author-url": "https://wildan.dev",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "gray-matter": "^4.0.3",
30
+ "next-mdx-remote": "^6.0.0",
31
+ "rehype-autolink-headings": "^7.1.0",
32
+ "rehype-code-titles": "^1.2.1",
33
+ "rehype-prism-plus": "^2.0.2",
34
+ "rehype-slug": "^6.0.0",
35
+ "remark-gfm": "^4.0.1",
36
+ "unist-util-visit": "^5.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.19.37",
40
+ "@types/react": "19.2.8",
41
+ "@types/unist": "^3.0.3",
42
+ "typescript": "^5.9.3"
43
+ }
44
+ }
package/src/compile.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { compileMDX } from "next-mdx-remote/rsc";
2
+ import type { Node, Parent } from "unist";
3
+ import { visit } from "unist-util-visit";
4
+ import remarkGfm from "remark-gfm";
5
+ import rehypePrism from "rehype-prism-plus";
6
+ import rehypeAutolinkHeadings from "rehype-autolink-headings";
7
+ import rehypeSlug from "rehype-slug";
8
+ import rehypeCodeTitles from "rehype-code-titles";
9
+ import type { MdxCompileResult } from "./types";
10
+
11
+ interface Element extends Node {
12
+ type: string;
13
+ tagName?: string;
14
+ properties?: Record<string, unknown> & {
15
+ className?: string[];
16
+ raw?: string;
17
+ };
18
+ children?: Node[];
19
+ raw?: string;
20
+ }
21
+
22
+ interface TextNode extends Node {
23
+ type: "text";
24
+ value: string;
25
+ }
26
+
27
+ type CompileMdxInput = Parameters<typeof compileMDX<Record<string, unknown>>>[0];
28
+ type CompileMdxOptions = NonNullable<CompileMdxInput["options"]>;
29
+ type CompilerMdxOptions = NonNullable<CompileMdxOptions["mdxOptions"]>;
30
+
31
+ export type ParseMdxOptions = {
32
+ components?: CompileMdxInput["components"];
33
+ rehypePlugins?: CompilerMdxOptions["rehypePlugins"];
34
+ remarkPlugins?: CompilerMdxOptions["remarkPlugins"];
35
+ };
36
+
37
+ export const handleCodeTitles = () => (tree: Node) => {
38
+ visit(tree, "element", (node: Element, index: number | null, parent: Parent | null) => {
39
+ if (!parent || index === null || node.tagName !== "div") {
40
+ return;
41
+ }
42
+
43
+ const isTitleDiv = node.properties?.className?.includes("rehype-code-title");
44
+ if (!isTitleDiv) {
45
+ return;
46
+ }
47
+
48
+ let nextElement: Element | null = null;
49
+ for (let i = index + 1; i < parent.children.length; i++) {
50
+ const sibling = parent.children[i];
51
+ if (sibling.type === "element") {
52
+ nextElement = sibling as Element;
53
+ break;
54
+ }
55
+ }
56
+
57
+ if (nextElement?.tagName === "pre") {
58
+ const titleNode = node.children?.[0] as TextNode;
59
+ if (titleNode?.type === "text") {
60
+ if (!nextElement.properties) {
61
+ nextElement.properties = {};
62
+ }
63
+ nextElement.properties["data-title"] = titleNode.value;
64
+ parent.children.splice(index, 1);
65
+ return index;
66
+ }
67
+ }
68
+ });
69
+ };
70
+
71
+ export const preProcess = () => (tree: Node) => {
72
+ visit(tree, (node: Node) => {
73
+ const element = node as Element;
74
+ if (element?.type === "element" && element?.tagName === "pre" && element.children) {
75
+ const [codeEl] = element.children as Element[];
76
+ if (codeEl.tagName !== "code" || !codeEl.children?.[0]) return;
77
+
78
+ const textNode = codeEl.children[0] as TextNode;
79
+ if (textNode.type === "text" && textNode.value) {
80
+ element.raw = textNode.value;
81
+ }
82
+ }
83
+ });
84
+ };
85
+
86
+ export const postProcess = () => (tree: Node) => {
87
+ visit(tree, "element", (node: Node) => {
88
+ const element = node as Element;
89
+ if (element?.type === "element" && element?.tagName === "pre") {
90
+ if (element.properties && element.raw) {
91
+ element.properties.raw = element.raw;
92
+ }
93
+ }
94
+ });
95
+ };
96
+
97
+ export function createDefaultRehypePlugins(): unknown[] {
98
+ return [
99
+ preProcess,
100
+ rehypeCodeTitles,
101
+ handleCodeTitles,
102
+ rehypePrism,
103
+ rehypeSlug,
104
+ rehypeAutolinkHeadings,
105
+ postProcess,
106
+ ];
107
+ }
108
+
109
+ export function createDefaultRemarkPlugins(): unknown[] {
110
+ return [remarkGfm];
111
+ }
112
+
113
+ export async function parseMdx<Frontmatter>(
114
+ rawMdx: string,
115
+ options: ParseMdxOptions = {}
116
+ ): Promise<MdxCompileResult<Frontmatter>> {
117
+ const rehypePlugins = options.rehypePlugins ?? (createDefaultRehypePlugins() as CompilerMdxOptions["rehypePlugins"]);
118
+ const remarkPlugins = options.remarkPlugins ?? (createDefaultRemarkPlugins() as CompilerMdxOptions["remarkPlugins"]);
119
+
120
+ return await compileMDX<Frontmatter>({
121
+ source: rawMdx,
122
+ options: {
123
+ parseFrontmatter: true,
124
+ mdxOptions: {
125
+ rehypePlugins,
126
+ remarkPlugins,
127
+ },
128
+ },
129
+ components: options.components,
130
+ });
131
+ }
package/src/content.ts ADDED
@@ -0,0 +1,129 @@
1
+ import path from "path";
2
+ import { promises as fs } from "fs";
3
+ import { extractFrontmatter, extractTocsFromRawMdx } from "./extract";
4
+ import { parseMdx, type ParseMdxOptions } from "./compile";
5
+ import type { MdxCompileResult, TocItem } from "./types";
6
+
7
+ type CacheFn = <T extends (...args: any[]) => any>(fn: T) => T;
8
+
9
+ export type ReadMdxFileResult = {
10
+ content: string;
11
+ filePath: string;
12
+ };
13
+
14
+ export type ParsedMdxFile<Frontmatter, T extends TocItem = TocItem> = {
15
+ content: string;
16
+ filePath: string;
17
+ frontmatter: Frontmatter;
18
+ tocs: T[];
19
+ };
20
+
21
+ export type CompiledMdxFile<Frontmatter, T extends TocItem = TocItem> = MdxCompileResult<Frontmatter> & {
22
+ filePath: string;
23
+ tocs: T[];
24
+ };
25
+
26
+ type ReadMdxBySlugOptions = {
27
+ rootDir?: string;
28
+ docsDir?: string;
29
+ };
30
+
31
+ export async function readMdxFileBySlug(slug: string, options: ReadMdxBySlugOptions = {}): Promise<ReadMdxFileResult> {
32
+ const docsDir = options.docsDir ?? "docs";
33
+ // Keep file-system path operations ignored by Turbopack tracing.
34
+ // The runtime path is constrained to docsDir and slug candidates below.
35
+ const docsRoot = options.rootDir
36
+ ? path.join(/*turbopackIgnore: true*/ options.rootDir, docsDir)
37
+ : path.join(/*turbopackIgnore: true*/ process.cwd(), docsDir);
38
+ const paths = [
39
+ path.join(/*turbopackIgnore: true*/ docsRoot, `${slug}.mdx`),
40
+ path.join(/*turbopackIgnore: true*/ docsRoot, slug, "index.mdx"),
41
+ ];
42
+
43
+ for (const p of paths) {
44
+ try {
45
+ const content = await fs.readFile(/*turbopackIgnore: true*/ p, "utf-8");
46
+ return {
47
+ content,
48
+ filePath: `${docsDir}/${path.relative(/*turbopackIgnore: true*/ docsRoot, p)}`,
49
+ };
50
+ } catch {
51
+ // ignore and try next candidate
52
+ }
53
+ }
54
+
55
+ throw new Error(`Could not find mdx file for slug: ${slug}`);
56
+ }
57
+
58
+ type ParseMdxFileOptions<T extends TocItem> = {
59
+ tocsExtractor?: (rawMdx: string) => T[];
60
+ };
61
+
62
+ export function parseMdxFile<Frontmatter, T extends TocItem = TocItem>(
63
+ raw: ReadMdxFileResult,
64
+ options: ParseMdxFileOptions<T> = {}
65
+ ): ParsedMdxFile<Frontmatter, T> {
66
+ const tocsExtractor = options.tocsExtractor ?? ((mdx) => extractTocsFromRawMdx(mdx) as T[]);
67
+
68
+ return {
69
+ content: raw.content,
70
+ filePath: raw.filePath,
71
+ frontmatter: extractFrontmatter<Frontmatter>(raw.content),
72
+ tocs: tocsExtractor(raw.content),
73
+ };
74
+ }
75
+
76
+ export async function compileParsedMdxFile<Frontmatter, T extends TocItem = TocItem>(
77
+ parsed: ParsedMdxFile<Frontmatter, T>,
78
+ options: ParseMdxOptions = {}
79
+ ): Promise<CompiledMdxFile<Frontmatter, T>> {
80
+ const compiled = await parseMdx<Frontmatter>(parsed.content, options);
81
+
82
+ return {
83
+ ...compiled,
84
+ frontmatter: parsed.frontmatter,
85
+ filePath: parsed.filePath,
86
+ tocs: parsed.tocs,
87
+ };
88
+ }
89
+
90
+ export type CreateMdxContentServiceOptions<Frontmatter, T extends TocItem = TocItem> = {
91
+ parseOptions?: ParseMdxOptions;
92
+ readOptions?: ReadMdxBySlugOptions;
93
+ tocsExtractor?: (rawMdx: string) => T[];
94
+ cacheFn?: CacheFn;
95
+ };
96
+
97
+ export function createMdxContentService<Frontmatter, T extends TocItem = TocItem>(
98
+ options: CreateMdxContentServiceOptions<Frontmatter, T> = {}
99
+ ) {
100
+ const identityCache: CacheFn = (fn) => fn;
101
+ const cacheFn = options.cacheFn ?? identityCache;
102
+
103
+ const getParsedForSlug = cacheFn(async (slug: string): Promise<ParsedMdxFile<Frontmatter, T>> => {
104
+ const raw = await readMdxFileBySlug(slug, options.readOptions);
105
+ return parseMdxFile<Frontmatter, T>(raw, { tocsExtractor: options.tocsExtractor });
106
+ });
107
+
108
+ const getCompiledForSlug = cacheFn(async (slug: string): Promise<CompiledMdxFile<Frontmatter, T>> => {
109
+ const parsed = await getParsedForSlug(slug);
110
+ return compileParsedMdxFile<Frontmatter, T>(parsed, options.parseOptions);
111
+ });
112
+
113
+ async function getFrontmatterForSlug(slug: string): Promise<Frontmatter> {
114
+ const parsed = await getParsedForSlug(slug);
115
+ return parsed.frontmatter;
116
+ }
117
+
118
+ async function getTocsForSlug(slug: string): Promise<T[]> {
119
+ const parsed = await getParsedForSlug(slug);
120
+ return parsed.tocs;
121
+ }
122
+
123
+ return {
124
+ getParsedForSlug,
125
+ getCompiledForSlug,
126
+ getFrontmatterForSlug,
127
+ getTocsForSlug,
128
+ };
129
+ }
package/src/extract.ts ADDED
@@ -0,0 +1,44 @@
1
+ import matter from "gray-matter";
2
+ import type { TocItem } from "./types";
3
+
4
+ export function sluggify(text: string): string {
5
+ const slug = text.toLowerCase().replace(/\s+/g, "-");
6
+ return slug.replace(/[^a-z0-9-]/g, "");
7
+ }
8
+
9
+ export function extractTocsFromRawMdx(rawMdx: string): TocItem[] {
10
+ // Match code blocks, markdown headings, and <Release version="x.y.z" />.
11
+ const combinedRegex = /(```[\s\S]*?```)|^(#{2,4})\s(.+)$|<Release[^>]*version="([^"]+)"/gm;
12
+ const extractedHeadings: TocItem[] = [];
13
+
14
+ let match: RegExpExecArray | null;
15
+ while ((match = combinedRegex.exec(rawMdx)) !== null) {
16
+ if (match[1]) continue;
17
+
18
+ if (match[2]) {
19
+ const headingLevel = match[2].length;
20
+ const headingText = match[3].trim();
21
+ extractedHeadings.push({
22
+ level: headingLevel,
23
+ text: headingText,
24
+ href: `#${sluggify(headingText)}`,
25
+ });
26
+ continue;
27
+ }
28
+
29
+ if (match[4]) {
30
+ const version = match[4];
31
+ extractedHeadings.push({
32
+ level: 2,
33
+ text: `v${version}`,
34
+ href: `#${version}`,
35
+ });
36
+ }
37
+ }
38
+
39
+ return extractedHeadings;
40
+ }
41
+
42
+ export function extractFrontmatter<Frontmatter>(content: string): Frontmatter {
43
+ return matter(content).data as Frontmatter;
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ export type { TocItem, MdxCompileResult } from "./types";
2
+ export {
3
+ parseMdx,
4
+ preProcess,
5
+ postProcess,
6
+ handleCodeTitles,
7
+ createDefaultRehypePlugins,
8
+ createDefaultRemarkPlugins,
9
+ } from "./compile";
10
+ export { extractFrontmatter, extractTocsFromRawMdx, sluggify } from "./extract";
11
+ export type { ParseMdxOptions } from "./compile";
12
+ export {
13
+ readMdxFileBySlug,
14
+ parseMdxFile,
15
+ compileParsedMdxFile,
16
+ createMdxContentService,
17
+ } from "./content";
18
+ export type {
19
+ ReadMdxFileResult,
20
+ ParsedMdxFile,
21
+ CompiledMdxFile,
22
+ CreateMdxContentServiceOptions,
23
+ } from "./content";
package/src/types.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type TocItem = {
4
+ level: number;
5
+ text: string;
6
+ href: string;
7
+ };
8
+
9
+ export type MdxCompileResult<Frontmatter> = {
10
+ content: ReactNode;
11
+ frontmatter: Frontmatter;
12
+ scope?: Record<string, unknown>;
13
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "jsx": "preserve"
11
+ },
12
+ "include": [
13
+ "src/**/*"
14
+ ]
15
+ }