Skip to content

Commit 6a1a1a3

Browse files
committed
feat: add markdown support
1 parent 4da53c7 commit 6a1a1a3

9 files changed

Lines changed: 148 additions & 24 deletions

File tree

apps/site/next.mdx.use.client.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import Blockquote from '@node-core/ui-components/Common/Blockquote';
44
import MDXCodeTabs from '@node-core/ui-components/MDX/CodeTabs';
5+
import MDXTable from '@node-core/ui-components/Mdx/Table';
56

67
import DownloadButton from './components/Downloads/DownloadButton';
78
import BlogPostLink from './components/Downloads/Release/BlogPostLink';
@@ -71,4 +72,5 @@ export const htmlComponents = {
7172
pre: MDXCodeBox,
7273
// Renders an Image Component for `img` tags
7374
img: MDXImage,
75+
table: MDXTable,
7476
};

packages/ui-components/src/Common/Card/index.module.css

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@
99
dark:border-neutral-800;
1010

1111
.header {
12-
@apply text-sm
12+
@apply mb-2
13+
text-sm
1314
font-medium
1415
text-gray-500
1516
dark:text-gray-400;
1617
}
17-
18-
.body {
19-
@apply mt-2;
20-
}
2118
}

packages/ui-components/src/Common/Card/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ export const CardHeader: FC<CardProps> = ({ className, ...props }) => {
1313
return <div className={classNames(styles.header, className)} {...props} />;
1414
};
1515

16-
export const CardBody: FC<CardProps> = ({ className, ...props }) => {
17-
return <div className={classNames(styles.body, className)} {...props} />;
16+
export const CardBody: FC<CardProps> = props => {
17+
return <div {...props} />;
1818
};

packages/ui-components/src/Common/ResponsiveTable/DesktopTable/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { ResponsiveTableProps, TableData } from '..';
1+
import type { TableData } from '#ui/types';
2+
3+
import type { ResponsiveTableProps } from '..';
24

35
function DesktopTable<T extends TableData>({
46
data,
@@ -15,8 +17,8 @@ function DesktopTable<T extends TableData>({
1517
</tr>
1618
</thead>
1719
<tbody>
18-
{data.map(row => (
19-
<tr key={getRowId(row)}>
20+
{data.map((row, index) => (
21+
<tr key={getRowId(row, index)}>
2022
{columns.map(column => (
2123
<td key={column.key}>{row[column.key]}</td>
2224
))}

packages/ui-components/src/Common/ResponsiveTable/MobileTable/index.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import type { ResponsiveTableProps, TableData } from '..';
1+
import type { TableData } from '#ui/types';
2+
3+
import type { ResponsiveTableProps } from '..';
24
import styles from './index.module.css';
35
import { Card, CardBody, CardHeader } from '../../Card';
46

7+
58
function MobileTable<T extends TableData>({
69
data,
710
columns,
@@ -10,9 +13,9 @@ function MobileTable<T extends TableData>({
1013
}: ResponsiveTableProps<T>) {
1114
return (
1215
<div role="table" className="space-y-4">
13-
{data.map(row => (
14-
<Card role="rowgroup" key={getRowId(row)}>
15-
<CardHeader>{getRowLabel(row)}</CardHeader>
16+
{data.map((row, index) => (
17+
<Card role="rowgroup" key={getRowId(row, index)}>
18+
{getRowLabel && <CardHeader>{getRowLabel(row)}</CardHeader>}
1619

1720
<CardBody role="row">
1821
{columns.map(column => (

packages/ui-components/src/Common/ResponsiveTable/index.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1+
import type { TableColumn, TableData } from '#ui/types';
2+
13
import DesktopTable from './DesktopTable';
24
import MobileTable from './MobileTable';
35

4-
type Column = {
5-
key: string;
6-
header: string;
7-
};
8-
9-
export type TableData = Record<string, React.ReactNode>;
10-
116
export interface ResponsiveTableProps<T extends TableData> {
127
data: Array<T>;
13-
columns: Array<Column>;
14-
getRowId: (row: T) => string;
15-
getRowLabel: (row: T) => string;
8+
columns: Array<TableColumn>;
9+
getRowId: (row: T, index: number) => string;
10+
getRowLabel?: (row: T) => string;
1611
}
1712

1813
function ResponsiveTable<T extends TableData>(props: ResponsiveTableProps<T>) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { ReactNode } from 'react';
2+
3+
import { parseTableStructure } from '#ui/util/table';
4+
5+
import ResponsiveTable from '../../Common/ResponsiveTable';
6+
7+
const Table = ({ children }: { children: ReactNode }) => {
8+
const { data, columns } = parseTableStructure(children);
9+
10+
return (
11+
<ResponsiveTable
12+
data={data}
13+
columns={columns}
14+
// We have to use index as row id fallback
15+
getRowId={(row, index) => String(index)}
16+
/>
17+
);
18+
};
19+
20+
export default Table;

packages/ui-components/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,10 @@ export type SimpleLocaleConfig = {
2323
localName: string;
2424
name: string;
2525
};
26+
27+
export type TableColumn = {
28+
key: string;
29+
header: string;
30+
};
31+
32+
export type TableData = Record<string, React.ReactNode>;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
Children,
3+
isValidElement,
4+
type ReactElement,
5+
type ReactNode,
6+
} from 'react';
7+
8+
import type { TableColumn, TableData } from '#ui/types';
9+
10+
const hasChildren = (props: unknown): props is { children: ReactNode } =>
11+
typeof props === 'object' && props !== null && 'children' in props;
12+
13+
const isTableElement = (
14+
child: ReactNode,
15+
tagName: string
16+
): child is ReactElement<{ children?: ReactNode }> =>
17+
isValidElement(child) && child.type === tagName;
18+
19+
const getTextContent = (node: ReactNode): string => {
20+
if (typeof node === 'string' || typeof node === 'number') return String(node);
21+
if (Array.isArray(node)) return node.map(getTextContent).join('');
22+
if (isValidElement(node) && hasChildren(node.props))
23+
return getTextContent(node.props.children);
24+
return '';
25+
};
26+
27+
const getColumnKey = (headerText: string, index: number): string =>
28+
headerText
29+
? headerText.toLowerCase().trim().replace(/\s+/g, '_')
30+
: `col_${index}`;
31+
32+
export const extractColumns = (thead: ReactElement): Array<TableColumn> => {
33+
if (!hasChildren(thead.props) || !thead.props.children) return [];
34+
35+
const headerRows = Children.toArray(thead.props.children);
36+
const firstRow = headerRows[0];
37+
if (!isValidElement(firstRow) || !hasChildren(firstRow.props)) return [];
38+
39+
const headerCells = Children.toArray(firstRow.props.children);
40+
41+
return headerCells
42+
.map((cell, index) => {
43+
if (!isValidElement(cell) || !hasChildren(cell.props)) return null;
44+
45+
const headerText = getTextContent(cell.props.children);
46+
const key = getColumnKey(headerText, index);
47+
48+
return {
49+
key: key,
50+
header: headerText,
51+
};
52+
})
53+
.filter((col): col is TableColumn => col !== null);
54+
};
55+
56+
export const extractData = (
57+
tbody: ReactElement,
58+
columns: Array<TableColumn>
59+
): Array<TableData> => {
60+
if (!hasChildren(tbody.props) || !tbody.props.children) return [];
61+
62+
const bodyRows = Children.toArray(tbody.props.children);
63+
64+
return bodyRows
65+
.map(row => {
66+
if (!isValidElement(row) || !hasChildren(row.props)) return null;
67+
68+
const cells = Children.toArray(row.props.children);
69+
const rowData: TableData = {};
70+
71+
cells.forEach((cell, i) => {
72+
if (isValidElement(cell) && hasChildren(cell.props) && columns[i]) {
73+
rowData[columns[i].key] = cell.props.children;
74+
}
75+
});
76+
77+
return rowData;
78+
})
79+
.filter((row): row is TableData => row !== null);
80+
};
81+
82+
export const parseTableStructure = (
83+
children: ReactNode
84+
): { columns: Array<TableColumn>; data: Array<TableData> } => {
85+
if (!children) return { columns: [], data: [] };
86+
87+
const nodes = Children.toArray(children);
88+
const thead = nodes.find(node => isTableElement(node, 'thead'));
89+
const tbody = nodes.find(node => isTableElement(node, 'tbody'));
90+
91+
if (!thead) throw new Error('Thead not found');
92+
if (!tbody) throw new Error('Tbody not found');
93+
94+
const columns = extractColumns(thead);
95+
const data = extractData(tbody, columns);
96+
97+
return { columns, data };
98+
};

0 commit comments

Comments
 (0)