@tinacms/astro 0.4.1 → 0.5.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/dist/types.d.ts +130 -5
- package/package.json +1 -1
- package/src/MdxNode.astro +14 -3
- package/src/MdxTableNode.astro +84 -0
- package/src/Node.astro +4 -0
- package/src/TableNode.astro +65 -0
- package/src/__tests__/TinaMarkdown.test.ts +47 -0
- package/src/__tests__/__snapshots__/TinaMarkdown.test.ts.snap +4 -0
- package/src/__tests__/fixtures/FancyTable.astro +3 -0
- package/src/__tests__/fixtures/mdx-table.json +44 -0
- package/src/__tests__/fixtures/table.json +60 -0
- package/src/types.ts +142 -3
package/dist/types.d.ts
CHANGED
|
@@ -35,10 +35,54 @@ export type CodeBlockElement = {
|
|
|
35
35
|
children: TextElement[];
|
|
36
36
|
}[];
|
|
37
37
|
};
|
|
38
|
-
|
|
38
|
+
/** Per-column horizontal alignment for table cells. */
|
|
39
|
+
export type TableAlign = 'left' | 'right' | 'center';
|
|
40
|
+
/** A paragraph — the block wrapper Tina puts around inline content. */
|
|
41
|
+
export type ParagraphElement = {
|
|
39
42
|
type: 'p';
|
|
40
43
|
children: InlineElement[];
|
|
41
|
-
}
|
|
44
|
+
};
|
|
45
|
+
/** A single table cell. Its content is wrapped in a paragraph by the parser. */
|
|
46
|
+
export type TableCellElement = {
|
|
47
|
+
type: 'td';
|
|
48
|
+
children: ParagraphElement[];
|
|
49
|
+
};
|
|
50
|
+
export type TableRowElement = {
|
|
51
|
+
type: 'tr';
|
|
52
|
+
children: TableCellElement[];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Native markdown table node (the shape `@tinacms/mdx` emits from a GFM
|
|
56
|
+
* table). Rows/cells are plain `tr`/`td`; per-column text-align lives on
|
|
57
|
+
* `props.align`. Mirrors `TableElement` in
|
|
58
|
+
* `packages/@tinacms/mdx/src/parse/plate.ts`.
|
|
59
|
+
*/
|
|
60
|
+
export type TableElement = {
|
|
61
|
+
type: 'table';
|
|
62
|
+
props?: {
|
|
63
|
+
align?: TableAlign[];
|
|
64
|
+
};
|
|
65
|
+
children: TableRowElement[];
|
|
66
|
+
};
|
|
67
|
+
/** A cell in the legacy MDX-flow table; its `value` is itself rich text. */
|
|
68
|
+
export type MdxTableCell = {
|
|
69
|
+
value: TinaRichTextContent;
|
|
70
|
+
};
|
|
71
|
+
export type MdxTableRow = {
|
|
72
|
+
tableCells: MdxTableCell[];
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Props of the legacy MDX-flow table — an `mdxJsxFlowElement` named `table`
|
|
76
|
+
* whose cells live on `props.tableRows` rather than as `tr`/`td` child nodes.
|
|
77
|
+
* Older editors produced this shape; `MdxTableNode.astro` still renders it.
|
|
78
|
+
*/
|
|
79
|
+
export type MdxTableProps = {
|
|
80
|
+
align?: TableAlign[];
|
|
81
|
+
/** When true, the first row is rendered as a `<thead>` of `<th>`. */
|
|
82
|
+
firstRowHeader?: boolean;
|
|
83
|
+
tableRows?: MdxTableRow[];
|
|
84
|
+
};
|
|
85
|
+
export type BlockElement = ParagraphElement | {
|
|
42
86
|
type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
43
87
|
children: InlineElement[];
|
|
44
88
|
} | {
|
|
@@ -57,7 +101,7 @@ export type BlockElement = {
|
|
|
57
101
|
type: 'hr';
|
|
58
102
|
} | {
|
|
59
103
|
type: 'break';
|
|
60
|
-
} | ImageElement | CodeBlockElement | {
|
|
104
|
+
} | ImageElement | CodeBlockElement | TableElement | {
|
|
61
105
|
type: 'maybe_mdx';
|
|
62
106
|
} | {
|
|
63
107
|
type: 'html';
|
|
@@ -88,5 +132,86 @@ export type MdxElement = {
|
|
|
88
132
|
children?: TinaRichTextNode[];
|
|
89
133
|
};
|
|
90
134
|
export type TinaRichTextContent = TinaRichTextRoot | TinaRichTextNode[] | null | undefined;
|
|
91
|
-
/**
|
|
92
|
-
|
|
135
|
+
/**
|
|
136
|
+
* An Astro (or framework) component that accepts props `P`. Astro's language
|
|
137
|
+
* server types an imported `.astro` component as `(props: Props) => any`, so
|
|
138
|
+
* this both accepts such a component and checks that its `Props` match what
|
|
139
|
+
* the renderer passes to the override.
|
|
140
|
+
*/
|
|
141
|
+
export type AstroComponentWithProps<P> = (props: P) => any;
|
|
142
|
+
/** Props passed to an `a` (link) override. */
|
|
143
|
+
export interface LinkComponentProps {
|
|
144
|
+
url: string;
|
|
145
|
+
}
|
|
146
|
+
/** Props passed to an `img` (image) override. */
|
|
147
|
+
export interface ImageComponentProps {
|
|
148
|
+
url: string;
|
|
149
|
+
alt?: string;
|
|
150
|
+
caption?: string;
|
|
151
|
+
}
|
|
152
|
+
/** Props passed to a `code_block` override. */
|
|
153
|
+
export interface CodeBlockComponentProps {
|
|
154
|
+
value: string;
|
|
155
|
+
lang?: string;
|
|
156
|
+
}
|
|
157
|
+
/** Props passed to `html` / `html_inline` overrides. */
|
|
158
|
+
export interface HtmlComponentProps {
|
|
159
|
+
value: string;
|
|
160
|
+
}
|
|
161
|
+
/** Props passed to a `table` override (native table node). */
|
|
162
|
+
export interface TableComponentProps {
|
|
163
|
+
node: TableElement;
|
|
164
|
+
}
|
|
165
|
+
/** Props passed to `td` / `th` overrides. */
|
|
166
|
+
export interface TableCellComponentProps {
|
|
167
|
+
align?: TableAlign;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Map of component name → Astro component, passed to `<TinaMarkdown>`.
|
|
171
|
+
*
|
|
172
|
+
* The built-in keys below are suggested by editor autocomplete and override
|
|
173
|
+
* the matching default element/tag; the named-prop overrides are typed with
|
|
174
|
+
* the exact props the renderer passes (e.g. `code_block` receives
|
|
175
|
+
* `{ value, lang }`).
|
|
176
|
+
*
|
|
177
|
+
* Custom rich-text **templates** render as mdxJsx nodes dispatched by `name`,
|
|
178
|
+
* with their fields passed as props. Register one under its template name.
|
|
179
|
+
* By default a custom key accepts any component; supply the optional `Custom`
|
|
180
|
+
* type param to type them precisely and have the registration checked:
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* // template `cta` with a `title: string` field
|
|
184
|
+
* const components: CustomComponentsMap<{ cta: { title: string } }> = {
|
|
185
|
+
* cta: Cta, // ← Cta's Props must be assignable from `{ title: string }`
|
|
186
|
+
* };
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export type CustomComponentsMap<Custom extends Record<string, object> = Record<never, never>> = {
|
|
190
|
+
p?: AstroComponent;
|
|
191
|
+
h1?: AstroComponent;
|
|
192
|
+
h2?: AstroComponent;
|
|
193
|
+
h3?: AstroComponent;
|
|
194
|
+
h4?: AstroComponent;
|
|
195
|
+
h5?: AstroComponent;
|
|
196
|
+
h6?: AstroComponent;
|
|
197
|
+
ul?: AstroComponent;
|
|
198
|
+
ol?: AstroComponent;
|
|
199
|
+
li?: AstroComponent;
|
|
200
|
+
lic?: AstroComponent;
|
|
201
|
+
blockquote?: AstroComponent;
|
|
202
|
+
tr?: AstroComponent;
|
|
203
|
+
hr?: AstroComponent;
|
|
204
|
+
break?: AstroComponent;
|
|
205
|
+
a?: AstroComponentWithProps<LinkComponentProps>;
|
|
206
|
+
img?: AstroComponentWithProps<ImageComponentProps>;
|
|
207
|
+
code_block?: AstroComponentWithProps<CodeBlockComponentProps>;
|
|
208
|
+
html?: AstroComponentWithProps<HtmlComponentProps>;
|
|
209
|
+
html_inline?: AstroComponentWithProps<HtmlComponentProps>;
|
|
210
|
+
table?: AstroComponentWithProps<TableComponentProps>;
|
|
211
|
+
td?: AstroComponentWithProps<TableCellComponentProps>;
|
|
212
|
+
th?: AstroComponentWithProps<TableCellComponentProps>;
|
|
213
|
+
} & {
|
|
214
|
+
[K in keyof Custom]?: AstroComponentWithProps<Custom[K]>;
|
|
215
|
+
} & {
|
|
216
|
+
[name: string]: AstroComponent | undefined;
|
|
217
|
+
};
|
package/package.json
CHANGED
package/src/MdxNode.astro
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* mdxJsx{Flow,Text}Element nodes are dispatched by `node.name` — registered
|
|
4
4
|
* components on the `components` map render with the node's props spread in.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* When no component is registered for the name, an `mdxJsxFlowElement` named
|
|
7
|
+
* `table` renders the built-in legacy table (`MdxTableNode`), mirroring the
|
|
8
|
+
* React renderer. Any other unregistered name emits a visible placeholder so
|
|
9
|
+
* missing registrations surface during development.
|
|
7
10
|
*/
|
|
11
|
+
import MdxTableNode from './MdxTableNode.astro';
|
|
8
12
|
import type { CustomComponentsMap, MdxElement } from './types';
|
|
9
13
|
|
|
10
14
|
interface Props {
|
|
@@ -13,10 +17,17 @@ interface Props {
|
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
const { node, components } = Astro.props;
|
|
20
|
+
// Single place that guarantees mdx props are an object, so neither the
|
|
21
|
+
// component spread nor the legacy-table renderer downstream needs its own
|
|
22
|
+
// guard. Parsed content always sets props; this only defends malformed nodes.
|
|
23
|
+
const props: Record<string, unknown> = node.props ?? {};
|
|
16
24
|
const MdxComponent = components[node.name];
|
|
25
|
+
const isLegacyTable = !MdxComponent && node.name === 'table';
|
|
17
26
|
---
|
|
18
27
|
{MdxComponent ? (
|
|
19
|
-
<MdxComponent {...
|
|
28
|
+
<MdxComponent {...props} />
|
|
29
|
+
) : isLegacyTable ? (
|
|
30
|
+
<MdxTableNode node={{ ...node, props }} components={components} />
|
|
20
31
|
) : (
|
|
21
32
|
<span style="display:inline-block;padding:0.25rem 0.5rem;background:#fee;color:#900;border-radius:0.25rem;font-family:monospace;font-size:0.85em;">
|
|
22
33
|
No component provided for {node.name}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders the legacy MDX-flow table shape — an `mdxJsxFlowElement` named
|
|
4
|
+
* `table` whose cells live on `props.tableRows` (rather than as `tr`/`td`
|
|
5
|
+
* child nodes). Mirrors the matching branch of
|
|
6
|
+
* `packages/tinacms/src/rich-text/index.tsx`: when `props.firstRowHeader` is
|
|
7
|
+
* set the first row becomes a `<thead>` of `<th>` and the rest are `<td>` in
|
|
8
|
+
* `<tbody>`. Per-column alignment comes from `props.align`, and the `th` / `td`
|
|
9
|
+
* overrides apply. The `table` / `tr` overrides are intentionally NOT honored
|
|
10
|
+
* here: the `table` override is typed to receive a native `{ node:
|
|
11
|
+
* TableElement }`, which this legacy `MdxElement`-based shape can't provide.
|
|
12
|
+
*/
|
|
13
|
+
import TinaMarkdown from './TinaMarkdown.astro';
|
|
14
|
+
import type {
|
|
15
|
+
CustomComponentsMap,
|
|
16
|
+
InlineElement,
|
|
17
|
+
MdxElement,
|
|
18
|
+
MdxTableProps,
|
|
19
|
+
TinaRichTextContent,
|
|
20
|
+
} from './types';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
node: MdxElement;
|
|
24
|
+
components: CustomComponentsMap;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { node, components } = Astro.props;
|
|
28
|
+
// `node.props` is guaranteed to be an object — MdxNode defaults it before
|
|
29
|
+
// dispatching here — so no further guard is needed at this layer.
|
|
30
|
+
const props = node.props as MdxTableProps;
|
|
31
|
+
|
|
32
|
+
const align = props.align ?? [];
|
|
33
|
+
const allRows = props.tableRows ?? [];
|
|
34
|
+
const header = props.firstRowHeader ? allRows.at(0) : undefined;
|
|
35
|
+
const bodyRows = props.firstRowHeader ? allRows.slice(1) : allRows;
|
|
36
|
+
|
|
37
|
+
const ThOverride = components.th;
|
|
38
|
+
const TdOverride = components.td;
|
|
39
|
+
|
|
40
|
+
// A cell `value` is rich text; unwrap each wrapping paragraph to its inline
|
|
41
|
+
// nodes so a cell renders `<td>text</td>` rather than `<td><p>text</p></td>`.
|
|
42
|
+
const cellInline = (value: TinaRichTextContent): InlineElement[] => {
|
|
43
|
+
const nodes = Array.isArray(value) ? value : (value?.children ?? []);
|
|
44
|
+
return nodes.flatMap((n) => (n.type === 'p' ? n.children : []));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const cellStyle = (i: number) => `text-align:${align[i] ?? 'auto'}`;
|
|
48
|
+
---
|
|
49
|
+
<table>
|
|
50
|
+
{header && (
|
|
51
|
+
<thead>
|
|
52
|
+
<tr>
|
|
53
|
+
{(header.tableCells ?? []).map((cell, i) =>
|
|
54
|
+
ThOverride ? (
|
|
55
|
+
<ThOverride align={align[i]}>
|
|
56
|
+
<TinaMarkdown content={cellInline(cell.value)} components={components} />
|
|
57
|
+
</ThOverride>
|
|
58
|
+
) : (
|
|
59
|
+
<th style={cellStyle(i)}>
|
|
60
|
+
<TinaMarkdown content={cellInline(cell.value)} components={components} />
|
|
61
|
+
</th>
|
|
62
|
+
)
|
|
63
|
+
)}
|
|
64
|
+
</tr>
|
|
65
|
+
</thead>
|
|
66
|
+
)}
|
|
67
|
+
<tbody>
|
|
68
|
+
{bodyRows.map((row) => (
|
|
69
|
+
<tr>
|
|
70
|
+
{(row?.tableCells ?? []).map((cell, i) =>
|
|
71
|
+
TdOverride ? (
|
|
72
|
+
<TdOverride align={align[i]}>
|
|
73
|
+
<TinaMarkdown content={cellInline(cell.value)} components={components} />
|
|
74
|
+
</TdOverride>
|
|
75
|
+
) : (
|
|
76
|
+
<td style={cellStyle(i)}>
|
|
77
|
+
<TinaMarkdown content={cellInline(cell.value)} components={components} />
|
|
78
|
+
</td>
|
|
79
|
+
)
|
|
80
|
+
)}
|
|
81
|
+
</tr>
|
|
82
|
+
))}
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
package/src/Node.astro
CHANGED
|
@@ -12,12 +12,14 @@ import ImageNode from './ImageNode.astro';
|
|
|
12
12
|
import Leaf from './Leaf.astro';
|
|
13
13
|
import LinkNode from './LinkNode.astro';
|
|
14
14
|
import MdxNode from './MdxNode.astro';
|
|
15
|
+
import TableNode from './TableNode.astro';
|
|
15
16
|
import type {
|
|
16
17
|
CodeBlockElement,
|
|
17
18
|
CustomComponentsMap,
|
|
18
19
|
ImageElement,
|
|
19
20
|
LinkElement,
|
|
20
21
|
MdxElement,
|
|
22
|
+
TableElement,
|
|
21
23
|
TextElement,
|
|
22
24
|
TinaRichTextNode,
|
|
23
25
|
} from './types';
|
|
@@ -54,6 +56,8 @@ const containerTypes = new Set([
|
|
|
54
56
|
<ImageNode node={node as ImageElement} components={components} />
|
|
55
57
|
) : t === 'code_block' ? (
|
|
56
58
|
<CodeBlockNode node={node as CodeBlockElement} components={components} />
|
|
59
|
+
) : t === 'table' ? (
|
|
60
|
+
<TableNode node={node as TableElement} components={components} />
|
|
57
61
|
) : t === 'text' ? (
|
|
58
62
|
<Leaf node={node as TextElement} />
|
|
59
63
|
) : t === 'mdxJsxFlowElement' || t === 'mdxJsxTextElement' ? (
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders a native markdown `table` node. Mirrors the `case 'table'` branch
|
|
4
|
+
* in `packages/tinacms/src/rich-text/index.tsx`: every row goes into a single
|
|
5
|
+
* `<tbody>` as `<td>` cells (the first row is NOT promoted to a header — that
|
|
6
|
+
* matches the React renderer's native-table path), per-column alignment comes
|
|
7
|
+
* from `node.props.align`, and `components.table` / `.tr` / `.td` override the
|
|
8
|
+
* default tags. Each cell's content is the inline content of its paragraph,
|
|
9
|
+
* rendered without a wrapping `<p>` (the equivalent of React's `p` → `td`
|
|
10
|
+
* override).
|
|
11
|
+
*/
|
|
12
|
+
import TinaMarkdown from './TinaMarkdown.astro';
|
|
13
|
+
import type {
|
|
14
|
+
CustomComponentsMap,
|
|
15
|
+
InlineElement,
|
|
16
|
+
TableCellElement,
|
|
17
|
+
TableElement,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
node: TableElement;
|
|
22
|
+
components: CustomComponentsMap;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { node, components } = Astro.props;
|
|
26
|
+
|
|
27
|
+
const TableOverride = components.table;
|
|
28
|
+
const TrOverride = components.tr;
|
|
29
|
+
const TdOverride = components.td;
|
|
30
|
+
|
|
31
|
+
const align = node.props?.align ?? [];
|
|
32
|
+
const rows = node.children ?? [];
|
|
33
|
+
|
|
34
|
+
const TABLE_STYLE = 'border:1px solid #EDECF3';
|
|
35
|
+
const tdStyle = (i: number) =>
|
|
36
|
+
`text-align:${align[i] ?? 'auto'};border:1px solid #EDECF3;padding:0.25rem`;
|
|
37
|
+
|
|
38
|
+
// A cell wraps its content in a paragraph; unwrap to the inline nodes so the
|
|
39
|
+
// cell renders `<td>text</td>` rather than `<td><p>text</p></td>`.
|
|
40
|
+
const cellContent = (cell: TableCellElement): InlineElement[] =>
|
|
41
|
+
(cell?.children ?? []).flatMap((paragraph) => paragraph?.children ?? []);
|
|
42
|
+
|
|
43
|
+
const Table = TableOverride ?? 'table';
|
|
44
|
+
const tableProps = TableOverride ? { node } : { style: TABLE_STYLE };
|
|
45
|
+
const Tr = TrOverride ?? 'tr';
|
|
46
|
+
---
|
|
47
|
+
<Table {...tableProps}>
|
|
48
|
+
<tbody>
|
|
49
|
+
{rows.map((row) => (
|
|
50
|
+
<Tr>
|
|
51
|
+
{(row?.children ?? []).map((cell, i) =>
|
|
52
|
+
TdOverride ? (
|
|
53
|
+
<TdOverride align={align[i]}>
|
|
54
|
+
<TinaMarkdown content={cellContent(cell)} components={components} />
|
|
55
|
+
</TdOverride>
|
|
56
|
+
) : (
|
|
57
|
+
<td style={tdStyle(i)}>
|
|
58
|
+
<TinaMarkdown content={cellContent(cell)} components={components} />
|
|
59
|
+
</td>
|
|
60
|
+
)
|
|
61
|
+
)}
|
|
62
|
+
</Tr>
|
|
63
|
+
))}
|
|
64
|
+
</tbody>
|
|
65
|
+
</Table>
|
|
@@ -6,6 +6,8 @@ import codeBlock from './fixtures/code-block.json';
|
|
|
6
6
|
import leafMarks from './fixtures/leaf-marks.json';
|
|
7
7
|
import mdxJsxFlow from './fixtures/mdx-jsx-flow.json';
|
|
8
8
|
import mdxJsxText from './fixtures/mdx-jsx-text.json';
|
|
9
|
+
import mdxTable from './fixtures/mdx-table.json';
|
|
10
|
+
import table from './fixtures/table.json';
|
|
9
11
|
|
|
10
12
|
let container: AstroContainer;
|
|
11
13
|
|
|
@@ -84,6 +86,51 @@ describe('TinaMarkdown', () => {
|
|
|
84
86
|
});
|
|
85
87
|
});
|
|
86
88
|
|
|
89
|
+
describe('TinaMarkdown — tables', () => {
|
|
90
|
+
it('renders a native table node with rows, cells and column alignment', async () => {
|
|
91
|
+
const html = await render({ props: { content: table } });
|
|
92
|
+
expect(html).toMatchSnapshot();
|
|
93
|
+
expect(html).toContain('<table');
|
|
94
|
+
expect(html).toContain('<tbody>');
|
|
95
|
+
expect(html).toContain('<tr>');
|
|
96
|
+
expect(html).toContain('<td');
|
|
97
|
+
// Per-column alignment from props.align.
|
|
98
|
+
expect(html).toContain('text-align:left');
|
|
99
|
+
expect(html).toContain('text-align:center');
|
|
100
|
+
expect(html).toContain('text-align:right');
|
|
101
|
+
// Inline marks render inside cells.
|
|
102
|
+
expect(html).toContain('<strong>Ada</strong>');
|
|
103
|
+
// No <thead>/<th> on the native path (matches the React renderer).
|
|
104
|
+
expect(html).not.toContain('<thead>');
|
|
105
|
+
expect(html).not.toContain('<th');
|
|
106
|
+
// Cell content is not wrapped in an extra <p>.
|
|
107
|
+
expect(html).not.toMatch(/<td[^>]*><p[\s>]/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('overrides the table tag via components.table', async () => {
|
|
111
|
+
const FancyTable = await import('./fixtures/FancyTable.astro');
|
|
112
|
+
const html = await render({
|
|
113
|
+
props: { content: table, components: { table: FancyTable.default } },
|
|
114
|
+
});
|
|
115
|
+
expect(html).toContain('class="fancy-table"');
|
|
116
|
+
expect(html).toContain('Name');
|
|
117
|
+
expect(html).toContain('Eng');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('renders the legacy mdx-flow table with a header row', async () => {
|
|
121
|
+
const html = await render({ props: { content: mdxTable } });
|
|
122
|
+
expect(html).toMatchSnapshot();
|
|
123
|
+
expect(html).toContain('<thead>');
|
|
124
|
+
expect(html).toContain('<th');
|
|
125
|
+
expect(html).toContain('Header A');
|
|
126
|
+
expect(html).toContain('<tbody>');
|
|
127
|
+
expect(html).toContain('a1');
|
|
128
|
+
expect(html).toContain('text-align:left');
|
|
129
|
+
expect(html).toContain('text-align:right');
|
|
130
|
+
expect(html).not.toContain('No component provided for table');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
87
134
|
describe('TinaMarkdown — components map', () => {
|
|
88
135
|
it('dispatches a registered mdxJsx component by name', async () => {
|
|
89
136
|
const MyFeature = await import('./fixtures/MyFeature.astro');
|
|
@@ -5,3 +5,7 @@ exports[`TinaMarkdown > renders code blocks with language class 1`] = `"<pre><co
|
|
|
5
5
|
exports[`TinaMarkdown > renders nested leaf-mark formatting 1`] = `"<p>Some <strong>bold</strong> text</p><p>Some <strong><em>bold and emphasized</em></strong> text</p><p>Marks with <em>emphasized text nesting </em><strong><em>bold</em></strong><em> text</em></p><p><strong>Hello </strong><strong><em>world</em></strong><strong>, again</strong> <em>here</em></p><p>Some <code>inline code</code> examples</p><p><em>Hello </em><em><code>some code</code></em><em>, again</em></p><p><strong>Hello </strong><a href="https://example.com"><strong>world</strong></a></p><p><strong><em>Hello </em></strong><a href="https://example.com"><strong><em>world</em></strong></a><em> And some other text, which has a </em><a href="https://something.com"><em>link to something</em></a></p>"`;
|
|
6
6
|
|
|
7
7
|
exports[`TinaMarkdown > renders the basic kitchen-sink fixture 1`] = `"<h1>Heading</h1><p>A paragraph.</p><blockquote>A quote.</blockquote><ul><li><div>An item</div></li></ul><hr><p>A <a href="http://example.com">link</a>.</p><p><img src="https://get.svg.workers.dev" alt="Alt Text"></p>"`;
|
|
8
|
+
|
|
9
|
+
exports[`TinaMarkdown — tables > renders a native table node with rows, cells and column alignment 1`] = `"<table style="border:1px solid #EDECF3"> <tbody> <tr><td style="text-align:left;border:1px solid #EDECF3;padding:0.25rem"> Name </td><td style="text-align:center;border:1px solid #EDECF3;padding:0.25rem"> Role </td><td style="text-align:right;border:1px solid #EDECF3;padding:0.25rem"> Score </td></tr><tr><td style="text-align:left;border:1px solid #EDECF3;padding:0.25rem"> <strong>Ada</strong> </td><td style="text-align:center;border:1px solid #EDECF3;padding:0.25rem"> Eng </td><td style="text-align:right;border:1px solid #EDECF3;padding:0.25rem"> 99 </td></tr> </tbody> </table>"`;
|
|
10
|
+
|
|
11
|
+
exports[`TinaMarkdown — tables > renders the legacy mdx-flow table with a header row 1`] = `"<table> <thead> <tr> <th style="text-align:left"> Header A </th><th style="text-align:right"> Header B </th> </tr> </thead> <tbody> <tr> <td style="text-align:left"> a1 </td><td style="text-align:right"> b1 </td> </tr> </tbody> </table>"`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "root",
|
|
3
|
+
"children": [
|
|
4
|
+
{
|
|
5
|
+
"type": "mdxJsxFlowElement",
|
|
6
|
+
"name": "table",
|
|
7
|
+
"props": {
|
|
8
|
+
"firstRowHeader": true,
|
|
9
|
+
"align": ["left", "right"],
|
|
10
|
+
"tableRows": [
|
|
11
|
+
{
|
|
12
|
+
"tableCells": [
|
|
13
|
+
{
|
|
14
|
+
"value": [
|
|
15
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Header A" }] }
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"value": [
|
|
20
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Header B" }] }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"tableCells": [
|
|
27
|
+
{
|
|
28
|
+
"value": [
|
|
29
|
+
{ "type": "p", "children": [{ "type": "text", "text": "a1" }] }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"value": [
|
|
34
|
+
{ "type": "p", "children": [{ "type": "text", "text": "b1" }] }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"children": [{ "type": "text", "text": "" }]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "root",
|
|
3
|
+
"children": [
|
|
4
|
+
{
|
|
5
|
+
"type": "table",
|
|
6
|
+
"props": { "align": ["left", "center", "right"] },
|
|
7
|
+
"children": [
|
|
8
|
+
{
|
|
9
|
+
"type": "tr",
|
|
10
|
+
"children": [
|
|
11
|
+
{
|
|
12
|
+
"type": "td",
|
|
13
|
+
"children": [
|
|
14
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Name" }] }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "td",
|
|
19
|
+
"children": [
|
|
20
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Role" }] }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "td",
|
|
25
|
+
"children": [
|
|
26
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Score" }] }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "tr",
|
|
33
|
+
"children": [
|
|
34
|
+
{
|
|
35
|
+
"type": "td",
|
|
36
|
+
"children": [
|
|
37
|
+
{
|
|
38
|
+
"type": "p",
|
|
39
|
+
"children": [{ "type": "text", "text": "Ada", "bold": true }]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"type": "td",
|
|
45
|
+
"children": [
|
|
46
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Eng" }] }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "td",
|
|
51
|
+
"children": [
|
|
52
|
+
{ "type": "p", "children": [{ "type": "text", "text": "99" }] }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -44,8 +44,57 @@ export type CodeBlockElement = {
|
|
|
44
44
|
children?: { children: TextElement[] }[];
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
/** Per-column horizontal alignment for table cells. */
|
|
48
|
+
export type TableAlign = 'left' | 'right' | 'center';
|
|
49
|
+
|
|
50
|
+
/** A paragraph — the block wrapper Tina puts around inline content. */
|
|
51
|
+
export type ParagraphElement = {
|
|
52
|
+
type: 'p';
|
|
53
|
+
children: InlineElement[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** A single table cell. Its content is wrapped in a paragraph by the parser. */
|
|
57
|
+
export type TableCellElement = {
|
|
58
|
+
type: 'td';
|
|
59
|
+
children: ParagraphElement[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type TableRowElement = {
|
|
63
|
+
type: 'tr';
|
|
64
|
+
children: TableCellElement[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Native markdown table node (the shape `@tinacms/mdx` emits from a GFM
|
|
69
|
+
* table). Rows/cells are plain `tr`/`td`; per-column text-align lives on
|
|
70
|
+
* `props.align`. Mirrors `TableElement` in
|
|
71
|
+
* `packages/@tinacms/mdx/src/parse/plate.ts`.
|
|
72
|
+
*/
|
|
73
|
+
export type TableElement = {
|
|
74
|
+
type: 'table';
|
|
75
|
+
props?: { align?: TableAlign[] };
|
|
76
|
+
children: TableRowElement[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** A cell in the legacy MDX-flow table; its `value` is itself rich text. */
|
|
80
|
+
export type MdxTableCell = { value: TinaRichTextContent };
|
|
81
|
+
|
|
82
|
+
export type MdxTableRow = { tableCells: MdxTableCell[] };
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Props of the legacy MDX-flow table — an `mdxJsxFlowElement` named `table`
|
|
86
|
+
* whose cells live on `props.tableRows` rather than as `tr`/`td` child nodes.
|
|
87
|
+
* Older editors produced this shape; `MdxTableNode.astro` still renders it.
|
|
88
|
+
*/
|
|
89
|
+
export type MdxTableProps = {
|
|
90
|
+
align?: TableAlign[];
|
|
91
|
+
/** When true, the first row is rendered as a `<thead>` of `<th>`. */
|
|
92
|
+
firstRowHeader?: boolean;
|
|
93
|
+
tableRows?: MdxTableRow[];
|
|
94
|
+
};
|
|
95
|
+
|
|
47
96
|
export type BlockElement =
|
|
48
|
-
|
|
|
97
|
+
| ParagraphElement
|
|
49
98
|
| {
|
|
50
99
|
type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
51
100
|
children: InlineElement[];
|
|
@@ -58,6 +107,7 @@ export type BlockElement =
|
|
|
58
107
|
| { type: 'break' }
|
|
59
108
|
| ImageElement
|
|
60
109
|
| CodeBlockElement
|
|
110
|
+
| TableElement
|
|
61
111
|
| { type: 'maybe_mdx' }
|
|
62
112
|
| { type: 'html'; value: string }
|
|
63
113
|
| { type: 'invalid_markdown'; value: string };
|
|
@@ -93,5 +143,94 @@ export type TinaRichTextContent =
|
|
|
93
143
|
| null
|
|
94
144
|
| undefined;
|
|
95
145
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
146
|
+
/**
|
|
147
|
+
* An Astro (or framework) component that accepts props `P`. Astro's language
|
|
148
|
+
* server types an imported `.astro` component as `(props: Props) => any`, so
|
|
149
|
+
* this both accepts such a component and checks that its `Props` match what
|
|
150
|
+
* the renderer passes to the override.
|
|
151
|
+
*/
|
|
152
|
+
export type AstroComponentWithProps<P> = (props: P) => any;
|
|
153
|
+
|
|
154
|
+
/** Props passed to an `a` (link) override. */
|
|
155
|
+
export interface LinkComponentProps {
|
|
156
|
+
url: string;
|
|
157
|
+
}
|
|
158
|
+
/** Props passed to an `img` (image) override. */
|
|
159
|
+
export interface ImageComponentProps {
|
|
160
|
+
url: string;
|
|
161
|
+
alt?: string;
|
|
162
|
+
caption?: string;
|
|
163
|
+
}
|
|
164
|
+
/** Props passed to a `code_block` override. */
|
|
165
|
+
export interface CodeBlockComponentProps {
|
|
166
|
+
value: string;
|
|
167
|
+
lang?: string;
|
|
168
|
+
}
|
|
169
|
+
/** Props passed to `html` / `html_inline` overrides. */
|
|
170
|
+
export interface HtmlComponentProps {
|
|
171
|
+
value: string;
|
|
172
|
+
}
|
|
173
|
+
/** Props passed to a `table` override (native table node). */
|
|
174
|
+
export interface TableComponentProps {
|
|
175
|
+
node: TableElement;
|
|
176
|
+
}
|
|
177
|
+
/** Props passed to `td` / `th` overrides. */
|
|
178
|
+
export interface TableCellComponentProps {
|
|
179
|
+
align?: TableAlign;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Map of component name → Astro component, passed to `<TinaMarkdown>`.
|
|
184
|
+
*
|
|
185
|
+
* The built-in keys below are suggested by editor autocomplete and override
|
|
186
|
+
* the matching default element/tag; the named-prop overrides are typed with
|
|
187
|
+
* the exact props the renderer passes (e.g. `code_block` receives
|
|
188
|
+
* `{ value, lang }`).
|
|
189
|
+
*
|
|
190
|
+
* Custom rich-text **templates** render as mdxJsx nodes dispatched by `name`,
|
|
191
|
+
* with their fields passed as props. Register one under its template name.
|
|
192
|
+
* By default a custom key accepts any component; supply the optional `Custom`
|
|
193
|
+
* type param to type them precisely and have the registration checked:
|
|
194
|
+
*
|
|
195
|
+
* ```ts
|
|
196
|
+
* // template `cta` with a `title: string` field
|
|
197
|
+
* const components: CustomComponentsMap<{ cta: { title: string } }> = {
|
|
198
|
+
* cta: Cta, // ← Cta's Props must be assignable from `{ title: string }`
|
|
199
|
+
* };
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export type CustomComponentsMap<
|
|
203
|
+
Custom extends Record<string, object> = Record<never, never>,
|
|
204
|
+
> = {
|
|
205
|
+
// Block/inline overrides whose content comes through the default slot.
|
|
206
|
+
p?: AstroComponent;
|
|
207
|
+
h1?: AstroComponent;
|
|
208
|
+
h2?: AstroComponent;
|
|
209
|
+
h3?: AstroComponent;
|
|
210
|
+
h4?: AstroComponent;
|
|
211
|
+
h5?: AstroComponent;
|
|
212
|
+
h6?: AstroComponent;
|
|
213
|
+
ul?: AstroComponent;
|
|
214
|
+
ol?: AstroComponent;
|
|
215
|
+
li?: AstroComponent;
|
|
216
|
+
lic?: AstroComponent;
|
|
217
|
+
blockquote?: AstroComponent;
|
|
218
|
+
tr?: AstroComponent;
|
|
219
|
+
hr?: AstroComponent;
|
|
220
|
+
break?: AstroComponent;
|
|
221
|
+
// Overrides that receive named props.
|
|
222
|
+
a?: AstroComponentWithProps<LinkComponentProps>;
|
|
223
|
+
img?: AstroComponentWithProps<ImageComponentProps>;
|
|
224
|
+
code_block?: AstroComponentWithProps<CodeBlockComponentProps>;
|
|
225
|
+
html?: AstroComponentWithProps<HtmlComponentProps>;
|
|
226
|
+
html_inline?: AstroComponentWithProps<HtmlComponentProps>;
|
|
227
|
+
table?: AstroComponentWithProps<TableComponentProps>;
|
|
228
|
+
td?: AstroComponentWithProps<TableCellComponentProps>;
|
|
229
|
+
th?: AstroComponentWithProps<TableCellComponentProps>;
|
|
230
|
+
} & {
|
|
231
|
+
// Custom templates declared via the `Custom` param — typed by their props.
|
|
232
|
+
[K in keyof Custom]?: AstroComponentWithProps<Custom[K]>;
|
|
233
|
+
} & {
|
|
234
|
+
// Any other custom mdxJsx component name (matched by `node.name`).
|
|
235
|
+
[name: string]: AstroComponent | undefined;
|
|
236
|
+
};
|