@tinacms/astro 0.0.1 → 0.3.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 (75) hide show
  1. package/README.md +50 -20
  2. package/dist/bridge.d.ts +7 -1
  3. package/dist/bridge.js +1 -0
  4. package/dist/data.d.ts +19 -0
  5. package/dist/data.js +64 -0
  6. package/dist/data.test-d.d.ts +1 -0
  7. package/dist/experimental.d.ts +10 -0
  8. package/dist/experimental.js +87 -0
  9. package/dist/index.d.ts +36 -1
  10. package/dist/index.js +92 -2
  11. package/dist/integration.d.ts +5 -0
  12. package/dist/integration.js +69 -0
  13. package/dist/internal/admin-origin.d.ts +6 -0
  14. package/dist/internal/escape.d.ts +8 -0
  15. package/dist/internal/forms-store.d.ts +12 -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 +18 -0
  20. package/dist/island-route.js +87 -0
  21. package/dist/middleware.d.ts +3 -0
  22. package/dist/middleware.js +123 -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/dist/vite.d.ts +21 -0
  30. package/dist/vite.js +24 -0
  31. package/package.json +88 -17
  32. package/src/CodeBlockNode.astro +28 -0
  33. package/src/Container.astro +56 -0
  34. package/src/ImageNode.astro +17 -0
  35. package/src/LinkNode.astro +22 -0
  36. package/src/MdxNode.astro +24 -0
  37. package/src/Node.astro +11 -4
  38. package/src/TinaIsland.astro +44 -0
  39. package/src/TinaMarkdown.astro +8 -0
  40. package/src/__tests__/IslandStub.astro +8 -0
  41. package/src/__tests__/TinaIsland.test.ts +60 -0
  42. package/src/__tests__/TinaMarkdown.test.ts +112 -0
  43. package/src/__tests__/__snapshots__/TinaMarkdown.test.ts.snap +7 -0
  44. package/src/__tests__/fixtures/FancyHeading.astro +3 -0
  45. package/src/__tests__/fixtures/MyFeature.astro +4 -0
  46. package/src/__tests__/fixtures/basic-kitchen-sink.json +60 -0
  47. package/src/__tests__/fixtures/code-block.json +34 -0
  48. package/src/__tests__/fixtures/leaf-marks.json +199 -0
  49. package/src/__tests__/fixtures/mdx-jsx-flow.json +40 -0
  50. package/src/__tests__/fixtures/mdx-jsx-text.json +53 -0
  51. package/src/__tests__/forms-store.test.ts +70 -0
  52. package/src/__tests__/integration.test.ts +124 -0
  53. package/src/__tests__/island-route.test.ts +119 -0
  54. package/src/__tests__/middleware.test.ts +102 -0
  55. package/src/__tests__/sanitize.test.ts +75 -0
  56. package/src/__tests__/vite.test.ts +67 -0
  57. package/src/bridge.ts +7 -0
  58. package/src/data.test-d.ts +53 -0
  59. package/src/data.ts +73 -0
  60. package/src/experimental.ts +14 -0
  61. package/src/index.ts +54 -0
  62. package/src/integration.ts +94 -0
  63. package/src/internal/admin-origin.ts +19 -0
  64. package/src/internal/escape.ts +15 -0
  65. package/src/internal/forms-store.ts +50 -0
  66. package/src/internal/request-context.ts +23 -0
  67. package/src/is-edit-mode.ts +68 -0
  68. package/src/island-route.ts +109 -0
  69. package/src/middleware.ts +89 -0
  70. package/src/sanitize.ts +64 -0
  71. package/src/tina-field.ts +1 -0
  72. package/src/types.ts +97 -0
  73. package/src/vite.ts +40 -0
  74. package/dist/preview.d.ts +0 -1
  75. package/dist/preview.js +0 -1
@@ -0,0 +1,17 @@
1
+ ---
2
+ import { sanitizeImageSrc } from './sanitize';
3
+ import type { CustomComponentsMap, ImageElement } from './types';
4
+
5
+ interface Props {
6
+ node: ImageElement;
7
+ components: CustomComponentsMap;
8
+ }
9
+
10
+ const { node, components } = Astro.props;
11
+ const Override = components.img;
12
+ ---
13
+ {Override ? (
14
+ <Override url={node.url} alt={node.alt} caption={node.caption} />
15
+ ) : (
16
+ <img src={sanitizeImageSrc(node.url)} alt={node.alt ?? ''} />
17
+ )}
@@ -0,0 +1,22 @@
1
+ ---
2
+ import TinaMarkdown from './TinaMarkdown.astro';
3
+ import { sanitizeHref } from './sanitize';
4
+ import type { CustomComponentsMap, LinkElement } from './types';
5
+
6
+ interface Props {
7
+ node: LinkElement;
8
+ components: CustomComponentsMap;
9
+ }
10
+
11
+ const { node, components } = Astro.props;
12
+ const Override = components.a;
13
+ ---
14
+ {Override ? (
15
+ <Override url={node.url}>
16
+ <TinaMarkdown content={node.children} components={components} />
17
+ </Override>
18
+ ) : (
19
+ <a href={sanitizeHref(node.url)}>
20
+ <TinaMarkdown content={node.children} components={components} />
21
+ </a>
22
+ )}
@@ -0,0 +1,24 @@
1
+ ---
2
+ /**
3
+ * mdxJsx{Flow,Text}Element nodes are dispatched by `node.name` — registered
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.
7
+ */
8
+ import type { CustomComponentsMap, MdxElement } from './types';
9
+
10
+ interface Props {
11
+ node: MdxElement;
12
+ components: CustomComponentsMap;
13
+ }
14
+
15
+ const { node, components } = Astro.props;
16
+ const MdxComponent = components[node.name];
17
+ ---
18
+ {MdxComponent ? (
19
+ <MdxComponent {...(node.props ?? {})} />
20
+ ) : (
21
+ <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
+ No component provided for {node.name}
23
+ </span>
24
+ )}
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,44 @@
1
+ ---
2
+ import { adminOrigins } from './internal/admin-origin';
3
+
4
+ interface Props {
5
+ name: string;
6
+ wrapper: { tag: string; className?: string };
7
+ params?: Record<string, string | number>;
8
+ /** Mark at most one `<TinaIsland>` per page as the primary editable
9
+ * region so the admin opens it on load instead of the multi-document
10
+ * picker. SSR pages don't need this — the first `requestWithMetadata()`
11
+ * is treated as primary automatically. */
12
+ primary?: boolean;
13
+ }
14
+
15
+ const { name, wrapper, params, primary } = Astro.props;
16
+ const search = params
17
+ ? `?${new URLSearchParams(
18
+ Object.fromEntries(
19
+ Object.entries(params).map(([key, value]) => [key, String(value)])
20
+ )
21
+ ).toString()}`
22
+ : '';
23
+ const marker = `/tina-island/${name}${search}`;
24
+ const Tag = wrapper.tag as keyof HTMLElementTagNameMap;
25
+ const adminOrigin = adminOrigins();
26
+ ---
27
+ <!-- In-iframe bridge bootstrap for prerendered pages (the middleware only
28
+ injects on SSR responses). No-op outside the admin iframe; the
29
+ window flag dedupes across multiple <TinaIsland> on one page. -->
30
+ <script is:inline type="module" define:vars={{ adminOrigin }}>
31
+ if (window.self !== window.top && !window.__tinaBootstrap) {
32
+ window.__tinaBootstrap = 1;
33
+ const { init, refreshForms } = await import('/admin/bridge.js');
34
+ init(adminOrigin ? { adminOrigin } : undefined);
35
+ document.addEventListener('astro:page-load', refreshForms);
36
+ }
37
+ </script>
38
+ <Tag
39
+ class={wrapper.className}
40
+ data-tina-island={marker}
41
+ data-tina-island-primary={primary ? '' : undefined}
42
+ >
43
+ <slot />
44
+ </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,8 @@
1
+ ---
2
+ // Minimal component for island-route tests — echoes the prop it's given.
3
+ interface Props {
4
+ value: unknown;
5
+ }
6
+ const { value } = Astro.props;
7
+ ---
8
+ <span data-value={JSON.stringify(value)}>stub</span>
@@ -0,0 +1,60 @@
1
+ import { experimental_AstroContainer as AstroContainer } from 'astro/container';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import TinaIsland from '../TinaIsland.astro';
4
+
5
+ let container: AstroContainer;
6
+
7
+ beforeEach(async () => {
8
+ container = await AstroContainer.create();
9
+ });
10
+
11
+ const wrapper = { tag: 'article', className: 'prose' };
12
+
13
+ describe('TinaIsland', () => {
14
+ it('emits the data-tina-island marker with encoded params', async () => {
15
+ const html = await container.renderToString(TinaIsland, {
16
+ props: { name: 'post', wrapper, params: { slug: 'hello-world' } },
17
+ slots: { default: 'BODY' },
18
+ });
19
+ expect(html).toContain(
20
+ 'data-tina-island="/tina-island/post?slug=hello-world"'
21
+ );
22
+ expect(html).toContain('BODY');
23
+ expect(html).toContain('class="prose"');
24
+ });
25
+
26
+ it('emits the guarded in-iframe bootstrap script', async () => {
27
+ const html = await container.renderToString(TinaIsland, {
28
+ props: { name: 'post', wrapper },
29
+ slots: { default: 'BODY' },
30
+ });
31
+ expect(html).toContain('window.__tinaBootstrap');
32
+ expect(html).toContain("import('/admin/bridge.js')");
33
+ expect(html).toContain('window.self !== window.top');
34
+ });
35
+
36
+ it('threads the resolved admin origin through define:vars', async () => {
37
+ const html = await container.renderToString(TinaIsland, {
38
+ props: { name: 'post', wrapper },
39
+ slots: { default: 'BODY' },
40
+ });
41
+ // `define:vars` emits the value resolved from PUBLIC_TINA_ADMIN_ORIGIN —
42
+ // `null` here since it's unset in the test env.
43
+ expect(html).toContain('adminOrigin = null');
44
+ expect(html).toContain('init(adminOrigin ? { adminOrigin } : undefined)');
45
+ });
46
+
47
+ it('marks the wrapper data-tina-island-primary only when primary is set', async () => {
48
+ const withoutPrimary = await container.renderToString(TinaIsland, {
49
+ props: { name: 'post', wrapper },
50
+ slots: { default: 'BODY' },
51
+ });
52
+ expect(withoutPrimary).not.toContain('data-tina-island-primary');
53
+
54
+ const withPrimary = await container.renderToString(TinaIsland, {
55
+ props: { name: 'post', wrapper, primary: true },
56
+ slots: { default: 'BODY' },
57
+ });
58
+ expect(withPrimary).toContain('data-tina-island-primary');
59
+ });
60
+ });
@@ -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
+ }