@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 CHANGED
@@ -35,10 +35,54 @@ export type CodeBlockElement = {
35
35
  children: TextElement[];
36
36
  }[];
37
37
  };
38
- export type BlockElement = {
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
- /** A map of mdxJsx name (or default tag override) → Astro component. */
92
- export type CustomComponentsMap = Record<string, AstroComponent>;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinacms/astro",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "src/TinaMarkdown.astro",
6
6
  "types": "dist/index.d.ts",
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
- * Unregistered names emit a visible placeholder so missing registrations
6
- * surface during development.
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 {...(node.props ?? {})} />
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,3 @@
1
+ ---
2
+ ---
3
+ <table class="fancy-table"><slot /></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
- | { type: 'p'; children: InlineElement[] }
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
- /** A map of mdxJsx name (or default tag override) → Astro component. */
97
- export type CustomComponentsMap = Record<string, AstroComponent>;
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
+ };