@xyd-js/content 0.1.0-xyd.10

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/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@xyd-js/content",
3
+ "version": "0.1.0-xyd.10",
4
+ "description": "",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ "./package.json": "./package.json",
9
+ ".": "./dist/index.js",
10
+ "./vite": "./dist/vite.js",
11
+ "./md": "./dist/md.js"
12
+ },
13
+ "dependencies": {
14
+ "@mdx-js/mdx": "^3.1.0",
15
+ "@mdx-js/rollup": "^3.0.1",
16
+ "estree-util-is-identifier-name": "^3.0.0",
17
+ "estree-util-value-to-estree": "^3.1.2",
18
+ "gray-matter": "^4.0.3",
19
+ "mdast": "^3.0.0",
20
+ "mdast-util-mdx": "^3.0.0",
21
+ "mdast-util-mdx-jsx": "^3.1.3",
22
+ "mdast-util-to-string": "^4.0.0",
23
+ "remark-directive": "^3.0.0",
24
+ "remark-frontmatter": "^5.0.0",
25
+ "remark-gfm": "^4.0.0",
26
+ "remark-mdx-frontmatter": "^5.0.0",
27
+ "unified": "^11.0.5",
28
+ "unist-util-visit": "^5.0.0",
29
+ "vfile": "^6.0.3",
30
+ "@xyd-js/core": "0.1.0-xyd.9"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.7.8",
34
+ "rimraf": "^3.0.2",
35
+ "tsup": "^8.3.0",
36
+ "typescript": "^4.5.5",
37
+ "vite": "^6.0.7"
38
+ },
39
+ "scripts": {
40
+ "clean": "rimraf build",
41
+ "prebuild": "pnpm clean",
42
+ "build": "tsup",
43
+ "watch": "tsup --watch"
44
+ }
45
+ }
@@ -0,0 +1,13 @@
1
+ import {RemarkMdxTocOptions} from "./plugins/mdToc";
2
+ import {defaultPlugins} from "./plugins"
3
+
4
+ export {RemarkMdxTocOptions} from "./plugins/mdToc";
5
+
6
+ export function mdOptions(toc: RemarkMdxTocOptions) {
7
+ return {
8
+ remarkPlugins: [
9
+ ...defaultPlugins(toc)
10
+ ],
11
+ rehypePlugins: []
12
+ }
13
+ }
@@ -0,0 +1,26 @@
1
+ import remarkFrontmatter from "remark-frontmatter";
2
+ import remarkMdxFrontmatter from "remark-mdx-frontmatter";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkDirective from 'remark-directive'
5
+
6
+ import {remarkMdxToc, RemarkMdxTocOptions} from "./mdToc";
7
+ import {remarkInjectCodeMeta} from "./mdCode";
8
+ import {extractThemeSettings} from "./mdThemeSettings";
9
+ import {extractPage} from "./mdPage";
10
+ import {mdCodeGroup} from "./mdCodeGroup";
11
+ import {remarkDirectiveWithMarkdown} from "./mdComponentDirective";
12
+
13
+ export function defaultPlugins(toc: RemarkMdxTocOptions) {
14
+ return [
15
+ remarkFrontmatter,
16
+ remarkMdxFrontmatter,
17
+ remarkGfm,
18
+ remarkDirective,
19
+ remarkMdxToc(toc),
20
+ remarkInjectCodeMeta,
21
+ extractThemeSettings,
22
+ extractPage,
23
+ mdCodeGroup,
24
+ remarkDirectiveWithMarkdown
25
+ ]
26
+ }
@@ -0,0 +1,19 @@
1
+ import {visit} from "unist-util-visit";
2
+
3
+ /**
4
+ * This plugin injects the code meta into the code node's data
5
+ * so that it can be used in the code block component
6
+ */
7
+ export function remarkInjectCodeMeta() {
8
+ return (tree: any) => {
9
+ visit(tree, 'code', (node) => {
10
+ if (node.meta) {
11
+ node.data = node.data || {};
12
+ node.data.hProperties = {
13
+ ...(node.data.hProperties || {}),
14
+ meta: node.meta,
15
+ };
16
+ }
17
+ });
18
+ };
19
+ }
@@ -0,0 +1,40 @@
1
+ import {visit} from 'unist-util-visit';
2
+
3
+ /**
4
+ * This plugin transforms a custom container directive into a JSX node
5
+ * https://github.com/remarkjs/remark-directive is needed to parse the container directive
6
+ */
7
+ export function mdCodeGroup() {
8
+ return (tree: any) => {
9
+ visit(tree, 'containerDirective', (node) => {
10
+ // TODO: is `code-group` ok name? -> rename to CodeSample? or use `mdComponentDirective` instead
11
+ if (node.name !== 'code-group') {
12
+ return
13
+ }
14
+
15
+ const description = node.attributes?.title || '';
16
+ const codeblocks = [];
17
+
18
+ for (const child of node.children) {
19
+ if (child.type === 'code') {
20
+ const meta = child.meta || '';
21
+ const value = child.value || '';
22
+ const lang = child.lang || '';
23
+
24
+ codeblocks.push({value, lang, meta});
25
+ }
26
+ }
27
+
28
+ // Add metadata to the node
29
+ node.data = {
30
+ hName: 'DirectiveCodeSample',
31
+ hProperties: {
32
+ description,
33
+ codeblocks: JSON.stringify(codeblocks),
34
+ },
35
+ };
36
+
37
+ node.children = [];
38
+ });
39
+ };
40
+ }
@@ -0,0 +1,141 @@
1
+ import {Plugin, unified} from "unified";
2
+ import remarkParse from "remark-parse";
3
+ import remarkMdx from "remark-mdx";
4
+ import {visit} from "unist-util-visit";
5
+ import {Node as UnistNode} from "unist";
6
+
7
+ function toPascalCase(str: string) {
8
+ return str
9
+ .replace(/([a-z])([A-Z])/g, "$1 $2") // Add space before capital letters
10
+ .replace(/[^a-zA-Z0-9]/g, " ") // Replace special characters with space
11
+ .split(" ") // Split by space
12
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) // Capitalize first letter
13
+ .join("");
14
+ }
15
+
16
+
17
+ const supportedDirectives: { [key: string]: boolean } = {
18
+ Details: true,
19
+ details: true,
20
+
21
+ Table: true,
22
+ table: true,
23
+
24
+ Subtitle: true,
25
+ subtitle: true,
26
+ }
27
+
28
+ const tableComponents: { [key: string]: boolean } = {
29
+ Table: true,
30
+ table: true
31
+ }
32
+
33
+ const parseMarkdown = (content: string) => {
34
+ const ast = unified()
35
+ .use(remarkParse)
36
+ .use(remarkMdx)
37
+ .parse(content);
38
+ return ast.children;
39
+ };
40
+
41
+ export const remarkDirectiveWithMarkdown: Plugin = () => {
42
+ return (tree: UnistNode) => {
43
+ visit(tree, 'containerDirective', (node: any) => {
44
+ if (!supportedDirectives[node.name]) {
45
+ return;
46
+ }
47
+
48
+ const isTable = tableComponents[node.name];
49
+ const attributes = [];
50
+
51
+ // TODO: MORE GENERIC CUZ IT HAS IMPL DETAILS OF TABLE
52
+ if (isTable) {
53
+ // TODO: support tsx tables like: [<>`Promise<Reference[]>`</>] ?
54
+ const tableData = JSON.parse(node.children[0].value);
55
+ const [header, ...rows] = tableData;
56
+
57
+ const jsxNode = {
58
+ type: 'mdxJsxFlowElement',
59
+ name: 'Table',
60
+ attributes: [],
61
+ children: [
62
+ {
63
+ type: 'mdxJsxFlowElement',
64
+ name: 'Table.Head',
65
+ attributes: [],
66
+ children: [
67
+ {
68
+ type: 'mdxJsxFlowElement',
69
+ name: 'Table.Tr',
70
+ attributes: [],
71
+ children: header.map((cell: string) => ({
72
+ type: 'mdxJsxFlowElement',
73
+ name: 'Table.Th',
74
+ attributes: [],
75
+ children: parseMarkdown(cell)
76
+ }))
77
+ }
78
+ ]
79
+ },
80
+ // TODO: Table.Cell ?
81
+ ...rows.map((row: string[]) => ({
82
+ type: 'mdxJsxFlowElement',
83
+ name: 'Table.Tr',
84
+ attributes: [],
85
+ children: row.map((cell: string) => ({
86
+ type: 'mdxJsxFlowElement',
87
+ name: 'Table.Td',
88
+ attributes: [],
89
+ children: parseMarkdown(cell)
90
+ }))
91
+ }))
92
+ ]
93
+ };
94
+
95
+ Object.assign(node, jsxNode);
96
+ return;
97
+ }
98
+
99
+ if (node.attributes) {
100
+ const jsxProps = []
101
+
102
+ for (let [key, value] of Object.entries(node.attributes)) {
103
+ if (typeof value === "string" && !value.startsWith("<")) {
104
+ attributes.push({
105
+ type: 'mdxJsxAttribute',
106
+ name: key,
107
+ value: value
108
+ });
109
+ } else {
110
+ jsxProps.push(`${key}={${value}}`)
111
+
112
+ }
113
+ }
114
+
115
+ const mdxString = `<Fragment ${jsxProps.join(" ")}></Fragment>`
116
+
117
+ const ast = unified()
118
+ .use(remarkParse)
119
+ .use(remarkMdx)
120
+ .parse(mdxString);
121
+
122
+ if (ast && ast.children[0] && ast.children[0].attributes) {
123
+ for (const attr of ast.children[0].attributes) {
124
+ // TODO: support markdown also e.g Hello `World` - currently it mus be: Hello <code>World</code>
125
+ attributes.push(attr);
126
+ }
127
+ }
128
+ }
129
+
130
+ const jsxNode = {
131
+ type: 'mdxJsxFlowElement',
132
+ name: toPascalCase(node.name),
133
+ attributes: attributes,
134
+ children: node.children
135
+ };
136
+
137
+ Object.assign(node, jsxNode);
138
+ });
139
+ }
140
+ }
141
+
@@ -0,0 +1,32 @@
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
+ /**
10
+ * This plugin extracts the `page` variable from the markdown file.
11
+ * This variable(`page`) is used to determine if theme should be dropped out.
12
+ *
13
+ * It means that if page=true, theme is not used, and we can use the markdown file as a standalone page.
14
+ */
15
+ export const extractPage: Plugin = () => {
16
+ return (tree: UnistNode) => {
17
+ visit(tree, 'exportNamedDeclaration', (node: any) => {
18
+ const declaration = node.declaration;
19
+
20
+ if (!declaration || !declaration.declarations || !declaration.declarations.length) {
21
+ return;
22
+ }
23
+
24
+ // seek declarations like export const page = true in md files
25
+ declaration.declarations.forEach((decl: any) => {
26
+ if (decl.id.name === 'page') {
27
+ global.page = decl.init as boolean;
28
+ }
29
+ });
30
+ });
31
+ };
32
+ };
@@ -0,0 +1,30 @@
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
+ /**
14
+ * This plugin adds the `themeSettings` variable to the global scope.
15
+ * This variable is used to determine the theme settings for the current page.
16
+ */
17
+ export const extractThemeSettings: Plugin = () => {
18
+ return (tree: UnistNode) => {
19
+ visit(tree, 'exportNamedDeclaration', (node: any) => {
20
+ const declaration = node.declaration;
21
+ if (declaration && declaration.declarations) {
22
+ declaration.declarations.forEach((decl: any) => {
23
+ if (decl.id.name === 'themeSettings') {
24
+ global.themeSettings = decl.init as ThemeSettings;
25
+ }
26
+ });
27
+ }
28
+ });
29
+ };
30
+ };
@@ -0,0 +1,133 @@
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
+ attributes: [],
112
+ source: null,
113
+ declaration: {
114
+ type: "VariableDeclaration",
115
+ kind: "const",
116
+ declarations: [
117
+ {
118
+ type: "VariableDeclarator",
119
+ id: {
120
+ type: "Identifier",
121
+ name
122
+ },
123
+ init: valueToEstree(toc)
124
+ }
125
+ ]
126
+ }
127
+ }
128
+ ]
129
+ }
130
+ }
131
+ };
132
+ mdast.children.unshift(tocExport);
133
+ };
@@ -0,0 +1,14 @@
1
+ import type {Plugin} from 'rollup';
2
+ import mdx from '@mdx-js/rollup';
3
+
4
+ import {RemarkMdxTocOptions, mdOptions} from "../md";
5
+
6
+ export interface VitePluginInterface {
7
+ toc: RemarkMdxTocOptions
8
+ }
9
+
10
+ export function vitePlugins(options: VitePluginInterface): Plugin[] {
11
+ return [
12
+ mdx(mdOptions(options.toc)),
13
+ ];
14
+ }
package/src/fs.ts ADDED
@@ -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 {mdOptions} from "../packages/md";
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 opt = mdOptions({
31
+ minDepth: 2 // TODO: configurable?
32
+ })
33
+
34
+ const compiled = await mdxCompile(vfile, {
35
+ remarkPlugins: opt.remarkPlugins,
36
+ rehypePlugins: opt.rehypePlugins,
37
+ recmaPlugins: [],
38
+ outputFormat: 'function-body',
39
+ development: false,
40
+ });
41
+
42
+ return String(compiled)
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export {
2
+ compileBySlug
3
+ } from "./fs"
4
+
5
+ export {
6
+ pageFrontMatters,
7
+ filterNavigationByLevels
8
+ } from "./navigation"