@tinacms/astro 0.0.1 → 0.2.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.
Files changed (64) hide show
  1. package/README.md +37 -20
  2. package/dist/bridge-route.d.ts +3 -0
  3. package/dist/bridge-route.js +22 -0
  4. package/dist/bridge.d.ts +7 -1
  5. package/dist/bridge.js +1 -0
  6. package/dist/data.d.ts +16 -0
  7. package/dist/data.js +59 -0
  8. package/dist/experimental.d.ts +10 -0
  9. package/dist/experimental.js +57 -0
  10. package/dist/index.d.ts +36 -1
  11. package/dist/index.js +87 -2
  12. package/dist/integration.d.ts +25 -0
  13. package/dist/integration.js +22 -0
  14. package/dist/internal/escape.d.ts +8 -0
  15. package/dist/internal/forms-store.d.ts +23 -0
  16. package/dist/internal/request-context.d.ts +16 -0
  17. package/dist/is-edit-mode.d.ts +32 -0
  18. package/dist/is-edit-mode.js +37 -0
  19. package/dist/island-route.d.ts +43 -0
  20. package/dist/island-route.js +57 -0
  21. package/dist/middleware.d.ts +20 -0
  22. package/dist/middleware.js +109 -0
  23. package/dist/sanitize.d.ts +12 -1
  24. package/dist/sanitize.js +6 -10
  25. package/dist/tina-field.d.ts +1 -1
  26. package/dist/tina-field.js +1 -0
  27. package/dist/types.d.ts +92 -1
  28. package/dist/types.js +0 -1
  29. package/package.json +89 -17
  30. package/src/CodeBlockNode.astro +28 -0
  31. package/src/Container.astro +56 -0
  32. package/src/ImageNode.astro +17 -0
  33. package/src/LinkNode.astro +22 -0
  34. package/src/MdxNode.astro +24 -0
  35. package/src/Node.astro +11 -4
  36. package/src/TinaIsland.astro +42 -0
  37. package/src/TinaMarkdown.astro +8 -0
  38. package/src/__tests__/TinaMarkdown.test.ts +112 -0
  39. package/src/__tests__/__snapshots__/TinaMarkdown.test.ts.snap +7 -0
  40. package/src/__tests__/fixtures/FancyHeading.astro +3 -0
  41. package/src/__tests__/fixtures/MyFeature.astro +4 -0
  42. package/src/__tests__/fixtures/basic-kitchen-sink.json +60 -0
  43. package/src/__tests__/fixtures/code-block.json +34 -0
  44. package/src/__tests__/fixtures/leaf-marks.json +199 -0
  45. package/src/__tests__/fixtures/mdx-jsx-flow.json +40 -0
  46. package/src/__tests__/fixtures/mdx-jsx-text.json +53 -0
  47. package/src/__tests__/sanitize.test.ts +75 -0
  48. package/src/bridge-route.ts +33 -0
  49. package/src/bridge.ts +7 -0
  50. package/src/data.ts +97 -0
  51. package/src/experimental.ts +14 -0
  52. package/src/index.ts +54 -0
  53. package/src/integration.ts +49 -0
  54. package/src/internal/escape.ts +15 -0
  55. package/src/internal/forms-store.ts +41 -0
  56. package/src/internal/request-context.ts +23 -0
  57. package/src/is-edit-mode.ts +68 -0
  58. package/src/island-route.ts +110 -0
  59. package/src/middleware.ts +118 -0
  60. package/src/sanitize.ts +64 -0
  61. package/src/tina-field.ts +1 -0
  62. package/src/types.ts +97 -0
  63. package/dist/preview.d.ts +0 -1
  64. package/dist/preview.js +0 -1
package/src/Node.astro CHANGED
@@ -13,7 +13,10 @@ import Leaf from './Leaf.astro';
13
13
  import LinkNode from './LinkNode.astro';
14
14
  import MdxNode from './MdxNode.astro';
15
15
  import type {
16
+ CodeBlockElement,
16
17
  CustomComponentsMap,
18
+ ImageElement,
19
+ LinkElement,
17
20
  MdxElement,
18
21
  TextElement,
19
22
  TinaRichTextNode,
@@ -46,11 +49,11 @@ const containerTypes = new Set([
46
49
  {containerTypes.has(t) ? (
47
50
  <Container node={node as TinaRichTextNode & { children: TinaRichTextNode[] }} components={components} />
48
51
  ) : t === 'a' ? (
49
- <LinkNode node={node as { url: string; children: TinaRichTextNode[] }} components={components} />
52
+ <LinkNode node={node as LinkElement} components={components} />
50
53
  ) : t === 'img' ? (
51
- <ImageNode node={node as { url: string; alt?: string; caption?: string }} components={components} />
54
+ <ImageNode node={node as ImageElement} components={components} />
52
55
  ) : t === 'code_block' ? (
53
- <CodeBlockNode node={node as { value?: string; lang?: string; children?: { children: TextElement[] }[] }} components={components} />
56
+ <CodeBlockNode node={node as CodeBlockElement} components={components} />
54
57
  ) : t === 'text' ? (
55
58
  <Leaf node={node as TextElement} />
56
59
  ) : t === 'mdxJsxFlowElement' || t === 'mdxJsxTextElement' ? (
@@ -60,7 +63,11 @@ const containerTypes = new Set([
60
63
  ) : t === 'break' ? (
61
64
  Override ? <Override /> : <br />
62
65
  ) : t === 'html' || t === 'html_inline' ? (
63
- <Fragment set:html={(node as { value: string }).value} />
66
+ Override ? (
67
+ <Override value={(node as { value: string }).value} />
68
+ ) : (
69
+ (node as { value: string }).value
70
+ )
64
71
  ) : t === 'invalid_markdown' ? (
65
72
  <pre>{(node as { value: string }).value}</pre>
66
73
  ) : null}
@@ -0,0 +1,42 @@
1
+ ---
2
+ /**
3
+ * Marker wrapper for an editable region. Emits the `data-tina-island`
4
+ * attribute the bridge looks for, with the wrapper element/className taken
5
+ * from the registry entry — so the page-side element matches the server's
6
+ * `IslandConfig.wrapper` automatically.
7
+ *
8
+ * Usage:
9
+ *
10
+ * ```astro
11
+ * import TinaIsland from '@tinacms/astro/TinaIsland.astro';
12
+ * import { islands } from '../lib/islands';
13
+ *
14
+ * <TinaIsland name="post" wrapper={islands.post.wrapper} params={{ slug }}>
15
+ * <PostBody data={data} />
16
+ * </TinaIsland>
17
+ * ```
18
+ *
19
+ * `name` must match a key in the `IslandRegistry` you passed to
20
+ * `experimental_createIslandRoute`. `params` is encoded as URL search
21
+ * params and forwarded to the route handler.
22
+ */
23
+ interface Props {
24
+ name: string;
25
+ wrapper: { tag: string; className?: string };
26
+ params?: Record<string, string | number>;
27
+ }
28
+
29
+ const { name, wrapper, params } = Astro.props;
30
+ const search = params
31
+ ? `?${new URLSearchParams(
32
+ Object.fromEntries(
33
+ Object.entries(params).map(([key, value]) => [key, String(value)])
34
+ )
35
+ ).toString()}`
36
+ : '';
37
+ const marker = `/tina-island/${name}${search}`;
38
+ const Tag = wrapper.tag as keyof HTMLElementTagNameMap;
39
+ ---
40
+ <Tag class={wrapper.className} data-tina-island={marker}>
41
+ <slot />
42
+ </Tag>
@@ -8,7 +8,15 @@
8
8
  * (mdxJsxFlowElement / mdxJsxTextElement). Default tags (p, h1-h6, ul, ol,
9
9
  * li, blockquote, etc.) can be overridden by registering the tag name on
10
10
  * the same map. Recurses via this same component for nested rich-text.
11
+ *
12
+ * This file is also the package's main runtime entry — Astro projects
13
+ * import `TinaMarkdown` (default) plus the `tina` / `tinaField` helpers
14
+ * from the same subpath. Re-exporting from the .astro file lets named
15
+ * imports resolve through the same path that serves the component.
11
16
  */
17
+ export { requestWithMetadata } from './data';
18
+ export { tinaField } from './tina-field';
19
+
12
20
  import Node from './Node.astro';
13
21
  import type { CustomComponentsMap, TinaRichTextContent } from './types';
14
22
 
@@ -0,0 +1,112 @@
1
+ import { experimental_AstroContainer as AstroContainer } from 'astro/container';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import TinaMarkdown from '../TinaMarkdown.astro';
4
+ import basicKitchenSink from './fixtures/basic-kitchen-sink.json';
5
+ import codeBlock from './fixtures/code-block.json';
6
+ import leafMarks from './fixtures/leaf-marks.json';
7
+ import mdxJsxFlow from './fixtures/mdx-jsx-flow.json';
8
+ import mdxJsxText from './fixtures/mdx-jsx-text.json';
9
+
10
+ let container: AstroContainer;
11
+
12
+ beforeEach(async () => {
13
+ container = await AstroContainer.create();
14
+ });
15
+
16
+ /**
17
+ * Astro injects dev-only `data-astro-source-file` / `data-astro-source-loc`
18
+ * attributes for editor source-mapping. Strip them so snapshots and string
19
+ * assertions stay stable across Astro versions and dev/prod modes.
20
+ */
21
+ const stripDevAttrs = (html: string) =>
22
+ html.replace(/\s+data-astro-source-(?:file|loc)="[^"]*"/g, '');
23
+
24
+ const render = async (props: Parameters<typeof container.renderToString>[1]) =>
25
+ stripDevAttrs(await container.renderToString(TinaMarkdown, props));
26
+
27
+ describe('TinaMarkdown', () => {
28
+ it('renders the basic kitchen-sink fixture', async () => {
29
+ const html = await render({ props: { content: basicKitchenSink } });
30
+ expect(html).toMatchSnapshot();
31
+ });
32
+
33
+ it('renders nested leaf-mark formatting', async () => {
34
+ const html = await render({ props: { content: leafMarks } });
35
+ expect(html).toMatchSnapshot();
36
+ expect(html).toContain('<strong>');
37
+ expect(html).toContain('<em>');
38
+ expect(html).toContain('<code>inline code</code>');
39
+ });
40
+
41
+ it('renders code blocks with language class', async () => {
42
+ const html = await render({ props: { content: codeBlock } });
43
+ expect(html).toMatchSnapshot();
44
+ expect(html).toMatch(/<pre><code class="language-javascript">/);
45
+ });
46
+
47
+ it('falls back to a placeholder when an mdxJsx component is unregistered', async () => {
48
+ const html = await render({ props: { content: mdxJsxFlow } });
49
+ expect(html).toContain('No component provided for someFeature');
50
+ });
51
+
52
+ it('falls back to a placeholder for unregistered inline mdxJsx', async () => {
53
+ const html = await render({ props: { content: mdxJsxText } });
54
+ expect(html).toMatch(/No component provided for/);
55
+ });
56
+
57
+ it('renders an empty array when content is null', async () => {
58
+ const html = await render({ props: { content: null } });
59
+ expect(html.trim()).toBe('');
60
+ });
61
+
62
+ it('accepts a bare array of nodes (not wrapped in root)', async () => {
63
+ const nodes = [{ type: 'p', children: [{ type: 'text', text: 'hello' }] }];
64
+ const html = await render({ props: { content: nodes } });
65
+ expect(html).toContain('<p>hello</p>');
66
+ });
67
+
68
+ it('renders sanitized hrefs on link nodes', async () => {
69
+ const nodes = [
70
+ {
71
+ type: 'p',
72
+ children: [
73
+ {
74
+ type: 'a',
75
+ url: 'javascript:alert(1)',
76
+ children: [{ type: 'text', text: 'click' }],
77
+ },
78
+ ],
79
+ },
80
+ ];
81
+ const html = await render({ props: { content: nodes } });
82
+ expect(html).not.toContain('javascript:');
83
+ expect(html).toContain('href="#"');
84
+ });
85
+ });
86
+
87
+ describe('TinaMarkdown — components map', () => {
88
+ it('dispatches a registered mdxJsx component by name', async () => {
89
+ const MyFeature = await import('./fixtures/MyFeature.astro');
90
+ const html = await render({
91
+ props: {
92
+ content: mdxJsxFlow,
93
+ components: { someFeature: MyFeature.default },
94
+ },
95
+ });
96
+ expect(html).toContain('<section data-test="my-feature">');
97
+ expect(html).not.toContain('No component provided for someFeature');
98
+ });
99
+
100
+ it('overrides a default tag (h1) when registered on the components map', async () => {
101
+ const FancyHeading = await import('./fixtures/FancyHeading.astro');
102
+ const nodes = [{ type: 'h1', children: [{ type: 'text', text: 'hello' }] }];
103
+ const html = await render({
104
+ props: {
105
+ content: nodes,
106
+ components: { h1: FancyHeading.default },
107
+ },
108
+ });
109
+ expect(html).toContain('class="fancy"');
110
+ expect(html).toContain('hello');
111
+ });
112
+ });
@@ -0,0 +1,7 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`TinaMarkdown > renders code blocks with language class 1`] = `"<pre><code class="language-javascript">const test = 123</code></pre><pre><code>some random code</code></pre>"`;
4
+
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
+
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>"`;
@@ -0,0 +1,3 @@
1
+ ---
2
+ ---
3
+ <h1 class="fancy"><slot /></h1>
@@ -0,0 +1,4 @@
1
+ ---
2
+ const props = Astro.props;
3
+ ---
4
+ <section data-test="my-feature">{JSON.stringify(props)}</section>
@@ -0,0 +1,60 @@
1
+ {
2
+ "type": "root",
3
+ "children": [
4
+ {
5
+ "type": "h1",
6
+ "children": [{ "type": "text", "text": "Heading" }]
7
+ },
8
+ {
9
+ "type": "p",
10
+ "children": [{ "type": "text", "text": "A paragraph." }]
11
+ },
12
+ {
13
+ "type": "blockquote",
14
+ "children": [{ "type": "text", "text": "A quote." }]
15
+ },
16
+ {
17
+ "type": "ul",
18
+ "children": [
19
+ {
20
+ "type": "li",
21
+ "children": [
22
+ {
23
+ "type": "lic",
24
+ "children": [{ "type": "text", "text": "An item" }]
25
+ }
26
+ ]
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ "type": "hr",
32
+ "children": [{ "type": "text", "text": "" }]
33
+ },
34
+ {
35
+ "type": "p",
36
+ "children": [
37
+ { "type": "text", "text": "A " },
38
+ {
39
+ "type": "a",
40
+ "url": "http://example.com",
41
+ "title": "Example",
42
+ "children": [{ "type": "text", "text": "link" }]
43
+ },
44
+ { "type": "text", "text": "." }
45
+ ]
46
+ },
47
+ {
48
+ "type": "p",
49
+ "children": [
50
+ {
51
+ "type": "img",
52
+ "url": "https://get.svg.workers.dev",
53
+ "alt": "Alt Text",
54
+ "caption": "Image Title",
55
+ "children": [{ "type": "text", "text": "" }]
56
+ }
57
+ ]
58
+ }
59
+ ]
60
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "type": "root",
3
+ "children": [
4
+ {
5
+ "type": "code_block",
6
+ "lang": "javascript",
7
+ "value": "const test = 123",
8
+ "children": [
9
+ {
10
+ "type": "code_line",
11
+ "children": [
12
+ {
13
+ "text": "const test = 123"
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ },
19
+ {
20
+ "type": "code_block",
21
+ "value": "some random code",
22
+ "children": [
23
+ {
24
+ "type": "code_line",
25
+ "children": [
26
+ {
27
+ "text": "some random code"
28
+ }
29
+ ]
30
+ }
31
+ ]
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,199 @@
1
+ {
2
+ "type": "root",
3
+ "children": [
4
+ {
5
+ "type": "p",
6
+ "children": [
7
+ {
8
+ "type": "text",
9
+ "text": "Some "
10
+ },
11
+ {
12
+ "type": "text",
13
+ "text": "bold",
14
+ "bold": true
15
+ },
16
+ {
17
+ "type": "text",
18
+ "text": " text"
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "type": "p",
24
+ "children": [
25
+ {
26
+ "type": "text",
27
+ "text": "Some "
28
+ },
29
+ {
30
+ "type": "text",
31
+ "text": "bold and emphasized",
32
+ "italic": true,
33
+ "bold": true
34
+ },
35
+ {
36
+ "type": "text",
37
+ "text": " text"
38
+ }
39
+ ]
40
+ },
41
+ {
42
+ "type": "p",
43
+ "children": [
44
+ {
45
+ "type": "text",
46
+ "text": "Marks with "
47
+ },
48
+ {
49
+ "type": "text",
50
+ "text": "emphasized text nesting ",
51
+ "italic": true
52
+ },
53
+ {
54
+ "type": "text",
55
+ "text": "bold",
56
+ "italic": true,
57
+ "bold": true
58
+ },
59
+ {
60
+ "type": "text",
61
+ "text": " text",
62
+ "italic": true
63
+ }
64
+ ]
65
+ },
66
+ {
67
+ "type": "p",
68
+ "children": [
69
+ {
70
+ "type": "text",
71
+ "text": "Hello ",
72
+ "bold": true
73
+ },
74
+ {
75
+ "type": "text",
76
+ "text": "world",
77
+ "bold": true,
78
+ "italic": true
79
+ },
80
+ {
81
+ "type": "text",
82
+ "text": ", again",
83
+ "bold": true
84
+ },
85
+ {
86
+ "type": "text",
87
+ "text": " "
88
+ },
89
+ {
90
+ "type": "text",
91
+ "text": "here",
92
+ "italic": true
93
+ }
94
+ ]
95
+ },
96
+ {
97
+ "type": "p",
98
+ "children": [
99
+ {
100
+ "type": "text",
101
+ "text": "Some "
102
+ },
103
+ {
104
+ "type": "text",
105
+ "text": "inline code",
106
+ "code": true
107
+ },
108
+ {
109
+ "type": "text",
110
+ "text": " examples"
111
+ }
112
+ ]
113
+ },
114
+ {
115
+ "type": "p",
116
+ "children": [
117
+ {
118
+ "type": "text",
119
+ "text": "Hello ",
120
+ "italic": true
121
+ },
122
+ {
123
+ "type": "text",
124
+ "text": "some code",
125
+ "code": true,
126
+ "italic": true
127
+ },
128
+ {
129
+ "type": "text",
130
+ "text": ", again",
131
+ "italic": true
132
+ }
133
+ ]
134
+ },
135
+ {
136
+ "type": "p",
137
+ "children": [
138
+ {
139
+ "type": "text",
140
+ "text": "Hello ",
141
+ "bold": true
142
+ },
143
+ {
144
+ "type": "a",
145
+ "url": "https://example.com",
146
+ "title": "Example Site",
147
+ "children": [
148
+ {
149
+ "type": "text",
150
+ "text": "world",
151
+ "bold": true
152
+ }
153
+ ]
154
+ }
155
+ ]
156
+ },
157
+ {
158
+ "type": "p",
159
+ "children": [
160
+ {
161
+ "type": "text",
162
+ "text": "Hello ",
163
+ "italic": true,
164
+ "bold": true
165
+ },
166
+ {
167
+ "type": "a",
168
+ "url": "https://example.com",
169
+ "title": "Example Site",
170
+ "children": [
171
+ {
172
+ "type": "text",
173
+ "text": "world",
174
+ "italic": true,
175
+ "bold": true
176
+ }
177
+ ]
178
+ },
179
+ {
180
+ "type": "text",
181
+ "text": " And some other text, which has a ",
182
+ "italic": true
183
+ },
184
+ {
185
+ "type": "a",
186
+ "url": "https://something.com",
187
+ "title": null,
188
+ "children": [
189
+ {
190
+ "type": "text",
191
+ "text": "link to something",
192
+ "italic": true
193
+ }
194
+ ]
195
+ }
196
+ ]
197
+ }
198
+ ]
199
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "type": "root",
3
+ "children": [
4
+ {
5
+ "type": "h1",
6
+ "children": [
7
+ {
8
+ "type": "text",
9
+ "text": "hello"
10
+ }
11
+ ]
12
+ },
13
+ {
14
+ "type": "mdxJsxFlowElement",
15
+ "name": "someFeature",
16
+ "children": [
17
+ {
18
+ "type": "text",
19
+ "text": ""
20
+ }
21
+ ],
22
+ "props": {
23
+ "children": {
24
+ "type": "root",
25
+ "children": [
26
+ {
27
+ "type": "p",
28
+ "children": [
29
+ {
30
+ "type": "text",
31
+ "text": "Testing this thing"
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ }
37
+ }
38
+ }
39
+ ]
40
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "type": "root",
3
+ "children": [
4
+ {
5
+ "type": "h1",
6
+ "children": [
7
+ {
8
+ "type": "text",
9
+ "text": "hello "
10
+ },
11
+ {
12
+ "type": "mdxJsxTextElement",
13
+ "name": "someFeature",
14
+ "children": [
15
+ {
16
+ "type": "text",
17
+ "text": ""
18
+ }
19
+ ],
20
+ "props": {
21
+ "_value": "abc"
22
+ }
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "type": "p",
28
+ "children": [
29
+ {
30
+ "type": "text",
31
+ "text": "When not separated by whitespace"
32
+ },
33
+ {
34
+ "type": "mdxJsxTextElement",
35
+ "name": "someFeature",
36
+ "children": [
37
+ {
38
+ "type": "text",
39
+ "text": ""
40
+ }
41
+ ],
42
+ "props": {
43
+ "_value": "123"
44
+ }
45
+ },
46
+ {
47
+ "type": "text",
48
+ "text": "it still works"
49
+ }
50
+ ]
51
+ }
52
+ ]
53
+ }