@xyd-js/content 0.1.0-xyd.2

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/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export {
2
+ compileBySlug
3
+ } from "./src/utils";
4
+
5
+ export {
6
+ mdxOptions
7
+ } from "./src/mdx/options";
8
+
9
+ export {vitePlugins} from "./src/vite-plugins";
10
+
package/navigation.ts ADDED
@@ -0,0 +1,4 @@
1
+ export {
2
+ pageFrontMatters,
3
+ filterNavigationByLevels
4
+ } from "./src/navigation";
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@xyd-js/content",
3
+ "version": "0.1.0-xyd.2",
4
+ "description": "",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ "./package.json": "./package.json",
9
+ ".": "./dist/index.js",
10
+ "./navigation": "./dist/navigation.js"
11
+ },
12
+ "dependencies": {
13
+ "@mdx-js/mdx": "^3.1.0",
14
+ "@mdx-js/rollup": "^3.0.1",
15
+ "estree-util-is-identifier-name": "^3.0.0",
16
+ "estree-util-value-to-estree": "^3.1.2",
17
+ "gray-matter": "^4.0.3",
18
+ "mdast": "^3.0.0",
19
+ "mdast-util-mdx": "^3.0.0",
20
+ "mdast-util-mdx-jsx": "^3.1.3",
21
+ "mdast-util-to-string": "^4.0.0",
22
+ "remark-frontmatter": "^5.0.0",
23
+ "remark-gfm": "^4.0.0",
24
+ "remark-mdx-frontmatter": "^5.0.0",
25
+ "unified": "^11.0.5",
26
+ "unist-util-visit": "^5.0.0",
27
+ "vfile": "^6.0.3",
28
+ "@xyd-js/core": "0.1.0-xyd.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.7.8",
32
+ "tsup": "^8.3.0"
33
+ },
34
+ "scripts": {
35
+ "clean": "rimraf build",
36
+ "prebuild": "pnpm clean",
37
+ "build": "tsup",
38
+ "watch": "tsup --watch"
39
+ }
40
+ }
@@ -0,0 +1,15 @@
1
+ import {visit} from "unist-util-visit";
2
+
3
+ export function remarkInjectCodeMeta() {
4
+ return (tree: any) => {
5
+ visit(tree, 'code', (node) => {
6
+ if (node.meta) {
7
+ node.data = node.data || {};
8
+ node.data.hProperties = {
9
+ ...(node.data.hProperties || {}),
10
+ meta: node.meta,
11
+ };
12
+ }
13
+ });
14
+ };
15
+ }
@@ -0,0 +1,23 @@
1
+ import remarkFrontmatter from "remark-frontmatter";
2
+ import remarkMdxFrontmatter from "remark-mdx-frontmatter";
3
+ import remarkGfm from "remark-gfm";
4
+
5
+ import {remarkMdxToc, RemarkMdxTocOptions} from "@/mdx/toc";
6
+ import {remarkInjectCodeMeta} from "@/mdx/code";
7
+ import {extractThemeSettings} from "@/mdx/themeSettings";
8
+ import {extractPage} from "@/mdx/page";
9
+
10
+ export function mdxOptions(toc: RemarkMdxTocOptions) {
11
+ return {
12
+ remarkPlugins: [
13
+ remarkFrontmatter,
14
+ remarkMdxFrontmatter,
15
+ remarkGfm,
16
+ remarkInjectCodeMeta,
17
+ remarkMdxToc(toc),
18
+ extractThemeSettings,
19
+ extractPage
20
+ ],
21
+ rehypePlugins: []
22
+ }
23
+ }
@@ -0,0 +1,22 @@
1
+ import {Plugin} from 'unified';
2
+ import {Node as UnistNode} from 'unist';
3
+ import {visit} from 'unist-util-visit';
4
+
5
+ declare global {
6
+ var page: boolean | null | undefined
7
+ }
8
+
9
+ export const extractPage: Plugin = () => {
10
+ return (tree: UnistNode) => {
11
+ visit(tree, 'exportNamedDeclaration', (node: any) => {
12
+ const declaration = node.declaration;
13
+ if (declaration && declaration.declarations) {
14
+ declaration.declarations.forEach((decl: any) => {
15
+ if (decl.id.name === 'page') {
16
+ global.page = decl.init as boolean;
17
+ }
18
+ });
19
+ }
20
+ });
21
+ };
22
+ };
@@ -0,0 +1,26 @@
1
+ import {Plugin} from 'unified';
2
+ import {Node as UnistNode} from 'unist';
3
+ import {visit} from 'unist-util-visit';
4
+
5
+ interface ThemeSettings {
6
+ bigArticle: boolean;
7
+ }
8
+
9
+ declare global {
10
+ var themeSettings: ThemeSettings | undefined;
11
+ }
12
+
13
+ export const extractThemeSettings: Plugin = () => {
14
+ return (tree: UnistNode) => {
15
+ visit(tree, 'exportNamedDeclaration', (node: any) => {
16
+ const declaration = node.declaration;
17
+ if (declaration && declaration.declarations) {
18
+ declaration.declarations.forEach((decl: any) => {
19
+ if (decl.id.name === 'themeSettings') {
20
+ global.themeSettings = decl.init as ThemeSettings;
21
+ }
22
+ });
23
+ }
24
+ });
25
+ };
26
+ };
package/src/mdx/toc.ts ADDED
@@ -0,0 +1,132 @@
1
+ import {Root, Heading} from "mdast";
2
+ import {MdxjsEsm} from "mdast-util-mdx";
3
+ import {Plugin} from "unified"; // TODO: use Plugin type
4
+ import {MdxJsxFlowElement, MdxJsxAttribute} from "mdast-util-mdx-jsx";
5
+
6
+ export type TocEntry = {
7
+ depth: number,
8
+ value: string,
9
+ attributes: { [key: string]: string },
10
+ children: TocEntry[]
11
+ };
12
+
13
+ export type CustomTag = {
14
+ name: RegExp,
15
+ depth: (name: string) => number
16
+ };
17
+
18
+ export interface RemarkMdxTocOptions {
19
+ name?: string,
20
+ customTags?: CustomTag[],
21
+ minDepth?: number
22
+ }
23
+
24
+ // TODO: fix any
25
+ export const remarkMdxToc = (options: RemarkMdxTocOptions): Plugin => () => async (ast: any) => {
26
+ const {visit} = await import("unist-util-visit");
27
+ const {toString} = await import("mdast-util-to-string");
28
+ const {valueToEstree} = await import('estree-util-value-to-estree')
29
+ const {name: isIdentifierName} = await import('estree-util-is-identifier-name');
30
+
31
+ const mdast = ast as Root;
32
+ const name = options.name ?? "toc";
33
+ if (!isIdentifierName(name)) {
34
+ throw new Error(`Invalid name for an identifier: ${name}`);
35
+ }
36
+
37
+ const toc: TocEntry[] = [];
38
+ const flatToc: TocEntry[] = [];
39
+ const createEntry = (node: Heading | MdxJsxFlowElement, depth: number): TocEntry => {
40
+ let attributes = (node.data || {}) as TocEntry['attributes'];
41
+ if (node.type === "mdxJsxFlowElement") {
42
+ attributes = Object.fromEntries(
43
+ node.attributes
44
+ .filter(attribute => attribute.type === 'mdxJsxAttribute' && typeof attribute.value === 'string')
45
+ .map(attribute => [(attribute as MdxJsxAttribute).name, attribute.value])
46
+ ) as TocEntry['attributes'];
47
+ }
48
+ return {
49
+ depth,
50
+ value: toString(node, {includeImageAlt: false}),
51
+ attributes,
52
+ children: []
53
+ }
54
+ };
55
+
56
+ visit(mdast, ["heading", "mdxJsxFlowElement"], node => {
57
+ let depth = 0;
58
+ if (node.type === "mdxJsxFlowElement") {
59
+ let valid = false;
60
+ if (/^h[1-6]$/.test(node.name || "")) {
61
+ valid = true;
62
+ depth = parseInt(node.name!.substring(1));
63
+ } else if (options.customTags) {
64
+ for (const tag of options.customTags) {
65
+ if (tag.name.test(node.name || "")) {
66
+ valid = true;
67
+ depth = tag.depth(node.name || "");
68
+ break;
69
+ }
70
+ }
71
+ }
72
+
73
+ if (!valid) {
74
+ return;
75
+ }
76
+ } else if (node.type === "heading") {
77
+ depth = node.depth;
78
+ } else {
79
+ return;
80
+ }
81
+
82
+ if (depth && (options?.minDepth && options.minDepth > depth)) {
83
+ return
84
+ }
85
+
86
+ const entry = createEntry(node, depth);
87
+ flatToc.push(entry);
88
+
89
+ let parent: TocEntry[] = toc;
90
+ for (let i = flatToc.length - 1; i >= 0; --i) {
91
+ const current = flatToc[i];
92
+ if (current.depth < entry.depth) {
93
+ parent = current.children;
94
+ break;
95
+ }
96
+ }
97
+ parent.push(entry);
98
+ });
99
+
100
+ const tocExport: MdxjsEsm = {
101
+ type: "mdxjsEsm",
102
+ value: "",
103
+ data: {
104
+ estree: {
105
+ type: "Program",
106
+ sourceType: "module",
107
+ body: [
108
+ {
109
+ type: "ExportNamedDeclaration",
110
+ specifiers: [],
111
+ source: null,
112
+ declaration: {
113
+ type: "VariableDeclaration",
114
+ kind: "const",
115
+ declarations: [
116
+ {
117
+ type: "VariableDeclarator",
118
+ id: {
119
+ type: "Identifier",
120
+ name
121
+ },
122
+ init: valueToEstree(toc)
123
+ }
124
+ ]
125
+ }
126
+ }
127
+ ]
128
+ }
129
+ }
130
+ };
131
+ mdast.children.unshift(tocExport);
132
+ };
@@ -0,0 +1,188 @@
1
+ import {promises as fs} from 'fs';
2
+ import fs2, {open} from 'fs';
3
+ import path from 'path';
4
+
5
+ import React from "react";
6
+ import remarkFrontmatter from "remark-frontmatter";
7
+ import remarkMdxFrontmatter from "remark-mdx-frontmatter";
8
+ import matter from 'gray-matter';
9
+ import {VFile} from "vfile";
10
+ import {compile as mdxCompile} from "@mdx-js/mdx";
11
+
12
+ import {FrontMatter, Sidebar, PageFrontMatter, Header} from "@xyd-js/core";
13
+
14
+ // TODO: better algorithm + data structures - since it's on build time it's not a big deal nevertheless it should be changed in the future
15
+
16
+ // pageFrontMatters gets frontmatters for given navigation
17
+ export async function pageFrontMatters(navigation: Sidebar[]): Promise<PageFrontMatter> {
18
+ const frontmatters: PageFrontMatter = {}
19
+
20
+ const promises: Promise<any>[] = []
21
+
22
+ function mapPages(page: string | Sidebar) {
23
+ if (typeof page !== "string") {
24
+ page.pages?.forEach(mapPages)
25
+ return
26
+ }
27
+
28
+ promises.push(job(page, frontmatters))
29
+ }
30
+
31
+ navigation.map(async (nav: Sidebar) => {
32
+ nav.pages?.forEach(mapPages)
33
+ })
34
+
35
+ await Promise.all(promises)
36
+
37
+ return frontmatters
38
+ }
39
+
40
+ // filterNavigation filter navigation items by top levels of 'header' configuration and current 'slug'
41
+ export function filterNavigationByLevels(
42
+ headers: Header[],
43
+ slug: string
44
+ ) {
45
+ const topLevelTabMatcher = headers?.reduce((acc: any, header) => {
46
+ const tabLevel = header?.url?.split("/")?.length
47
+
48
+ if (!tabLevel) {
49
+ return {
50
+ ...acc
51
+ }
52
+ }
53
+
54
+ if (!acc[tabLevel]) {
55
+ return {
56
+ ...acc,
57
+ [tabLevel]: new Set().add(header?.url)
58
+ }
59
+ }
60
+
61
+ return {
62
+ ...acc,
63
+ [tabLevel]: acc[tabLevel].add(header?.url)
64
+ }
65
+ }, {}) as { [level: number]: Set<string> }
66
+
67
+ return (nav: Sidebar) => {
68
+ let match = false
69
+
70
+ Object.keys(topLevelTabMatcher).forEach((levelStr) => {
71
+ if (match) {
72
+ return true
73
+ }
74
+ const level = parseInt(levelStr)
75
+ const findThisSlug = slug.split("/").filter(s => !!s).slice(0, level).join("/")
76
+
77
+ function findMatchedPage(page: string | Sidebar) {
78
+ if (typeof page !== "string") {
79
+ page.pages?.forEach(findMatchedPage)
80
+ return
81
+ }
82
+ const findThisPage = page.split("/").filter(p => !!p).slice(0, level).join("/")
83
+
84
+ const set = topLevelTabMatcher[level]
85
+
86
+ if (set.has(findThisPage) && findThisPage === findThisSlug) {
87
+ match = true
88
+ return true
89
+ }
90
+ }
91
+
92
+ nav?.pages?.forEach(findMatchedPage)
93
+ })
94
+
95
+ return match
96
+ }
97
+ }
98
+
99
+ async function getFrontmatter(filePath: string, chunkSize = 1024 * 5) { // 5 KB chunk
100
+ return new Promise((resolve, reject) => {
101
+ open(filePath, 'r', (err, fd) => {
102
+ if (err) return reject(err);
103
+
104
+ const buffer = Buffer.alloc(chunkSize);
105
+ fs2.read(fd, buffer, 0, chunkSize, 0, (err, bytesRead) => {
106
+ if (err) return reject(err);
107
+
108
+ const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, bytesRead);
109
+ const content = new TextDecoder('utf-8').decode(uint8Array);
110
+ const {data: frontmatter} = matter(content); // extract frontmatter
111
+ resolve(frontmatter);
112
+
113
+ fs2.close(fd, () => {
114
+ });
115
+ });
116
+ });
117
+ });
118
+ }
119
+
120
+ function mdxExport(code: string) {
121
+ const scope = {
122
+ Fragment: React.Fragment,
123
+ jsxs: React.createElement,
124
+ jsx: React.createElement,
125
+ jsxDEV: React.createElement,
126
+ }
127
+ const fn = new Function(...Object.keys(scope), code)
128
+ return fn(scope)
129
+ }
130
+
131
+ async function getFrontmatterV2(filePath: string): Promise<FrontMatter> {
132
+ const body = await fs.readFile(filePath, "utf-8");
133
+
134
+ const vfile = new VFile({
135
+ path: filePath,
136
+ value: body,
137
+ contents: body
138
+ });
139
+
140
+ const compiled = await mdxCompile(vfile, {
141
+ remarkPlugins: [
142
+ remarkFrontmatter,
143
+ remarkMdxFrontmatter
144
+ ],
145
+ rehypePlugins: [],
146
+ recmaPlugins: [],
147
+ outputFormat: 'function-body',
148
+ development: false,
149
+ });
150
+
151
+ const code = String(compiled)
152
+
153
+ const {
154
+ reactFrontmatter, // in the future same key?
155
+ frontmatter
156
+ } = mdxExport(code)
157
+
158
+ const matter: FrontMatter = frontmatter
159
+
160
+ if (reactFrontmatter) {
161
+ if (typeof reactFrontmatter?.title === "function") {
162
+ matter.title = {
163
+ code: reactFrontmatter.title.toString()
164
+ }
165
+ }
166
+ }
167
+
168
+ return matter
169
+ }
170
+
171
+ async function job(page: string, frontmatters: PageFrontMatter) {
172
+ // TODO: it can be cwd because on build time it has entire path?
173
+ let filePath = path.join(process.cwd(), `${page}.mdx`) // TODO: support md toos
174
+ try {
175
+ await fs.access(filePath)
176
+ } catch (e) {
177
+ try {
178
+ const mdPath = filePath.replace(".mdx", ".md")
179
+ await fs.access(mdPath)
180
+ filePath = mdPath
181
+ } catch (e) {
182
+ }
183
+ }
184
+
185
+ const matter = await getFrontmatterV2(filePath)
186
+
187
+ frontmatters[page] = matter
188
+ }
@@ -0,0 +1,43 @@
1
+ import {promises as fs} from "fs";
2
+ import path from "path";
3
+
4
+ import {VFile} from "vfile";
5
+ import {compile as mdxCompile} from "@mdx-js/mdx";
6
+
7
+ import {mdxOptions} from "@/mdx/options";
8
+
9
+ export async function compileBySlug(
10
+ slug: string,
11
+ mdx: boolean
12
+ ): Promise<string> {
13
+ // TODO: cwd ?
14
+ const filePath = path.join(process.cwd(), `${slug}.${mdx ? "mdx" : "md"}`)
15
+
16
+ await fs.access(filePath)
17
+
18
+ const content = await fs.readFile(filePath, "utf-8");
19
+
20
+ return await compile(content, filePath)
21
+ }
22
+
23
+ async function compile(content: string, filePath: string): Promise<string> {
24
+ const vfile = new VFile({
25
+ path: filePath,
26
+ value: content,
27
+ contents: content
28
+ });
29
+
30
+ const mdOptions = mdxOptions({
31
+ minDepth: 2 // TODO: configurable?
32
+ })
33
+
34
+ const compiled = await mdxCompile(vfile, {
35
+ remarkPlugins: mdOptions.remarkPlugins,
36
+ rehypePlugins: mdOptions.rehypePlugins,
37
+ recmaPlugins: [],
38
+ outputFormat: 'function-body',
39
+ development: false,
40
+ });
41
+
42
+ return String(compiled)
43
+ }
@@ -0,0 +1,15 @@
1
+ import type {Plugin} from 'rollup';
2
+ import mdx from '@mdx-js/rollup';
3
+
4
+ import {RemarkMdxTocOptions} from "@/mdx/toc";
5
+ import {mdxOptions} from "@/mdx/options";
6
+
7
+ export interface VitePluginInterface {
8
+ toc: RemarkMdxTocOptions
9
+ }
10
+
11
+ export function vitePlugins(options: VitePluginInterface): Plugin[] {
12
+ return [
13
+ mdx(mdxOptions(options.toc)),
14
+ ];
15
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/mdx/*": ["src/mdx/*"],
6
+ "@/navigation/*": ["src/navigation/*"],
7
+ "@/utils/*": ["src/utils/*"],
8
+ "@/vite-plugins/*": ["src/vite-plugins/*"]
9
+ },
10
+ "target": "ES2020",
11
+ "module": "ESNext",
12
+ "moduleResolution": "node",
13
+ "strict": true,
14
+ "esModuleInterop": true,
15
+ "skipLibCheck": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "outDir": "./dist",
18
+ "types": ["node", "estree", "vite"],
19
+ "declaration": true,
20
+ "declarationMap": true,
21
+ "incremental": true,
22
+ "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
23
+ },
24
+ "include": ["src/**/*.ts"],
25
+ "exclude": ["node_modules", "dist"]
26
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { defineConfig, Options } from 'tsup';
2
+
3
+ const config: Options = {
4
+ entry: {
5
+ index: 'index.ts',
6
+ navigation: 'navigation.ts',
7
+ },
8
+ dts: {
9
+ entry: {
10
+ index: 'index.ts',
11
+ navigation: 'navigation.ts',
12
+ },
13
+ resolve: true, // Resolve external types
14
+ },
15
+ format: ['esm'], // Output both ESM and CJS formats
16
+ target: 'node16', // Ensure compatibility with Node.js 16
17
+ splitting: false, // Disable code splitting
18
+ sourcemap: false, // Generate source maps
19
+ clean: true, // Clean the output directory before each build
20
+ // esbuildOptions: (options) => {
21
+ // options.platform = 'node'; // Ensure the platform is set to Node.js
22
+ // options.external = ['node:fs/promises']; // Mark 'node:fs/promises' as external
23
+ // options.loader = { '.js': 'jsx' }; // Ensure proper handling of .js files
24
+ // },
25
+ };
26
+
27
+ export default defineConfig(config);
package/vite.config.js ADDED
@@ -0,0 +1,53 @@
1
+ import {defineConfig} from 'vite';
2
+
3
+ export default defineConfig(async () => {
4
+ const mdx = await import('@mdx-js/rollup');
5
+ const remix = await import('@remix-run/dev');
6
+ const remarkFrontmatter = await import('remark-frontmatter');
7
+ const remarkMdxFrontmatter = await import('remark-mdx-frontmatter');
8
+ const remarkGfm = await import('remark-gfm');
9
+ const rehypePrettyCode = await import('rehype-pretty-code'); // TODO: for some reasons does not work
10
+ // const { remarkCodeHike, recmaCodeHike } = await import('codehike/mdx'); // TODO: delete because we use this inside components?
11
+
12
+ const settings = await import("./settings.json");
13
+
14
+ return {
15
+ optimizeDeps: {
16
+ include: ["react/jsx-runtime"],
17
+ },
18
+ resolve: {},
19
+ plugins: [
20
+ mdx.default({
21
+ // providerImportSource: '@mdx-js/react',
22
+ remarkPlugins: [
23
+ // remarkCodeHike,
24
+ remarkFrontmatter.default,
25
+ remarkMdxFrontmatter.default,
26
+ remarkGfm.default
27
+ ],
28
+ rehypePlugins: [
29
+ // recmaCodeHike,
30
+ // rehypePrettyCode.default, TODO: for some reasons does not work
31
+ ],
32
+ }),
33
+ remix.vitePlugin({
34
+ // routes(defineRoutes) {
35
+ // return defineRoutes((route) => {
36
+ // route(
37
+ // "authentication",
38
+ // "routes/_index.tsx",
39
+ // {id: 'routes/__main-index'},
40
+ // () => {
41
+ // route(
42
+ // "test",
43
+ // "routes/$page.tsx",
44
+ // {id: 'routes/__main-index-2'},
45
+ // );
46
+ // });
47
+ // });
48
+ // },
49
+ }),
50
+ // Add other plugins here
51
+ ],
52
+ };
53
+ });