@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.
- package/README.md +37 -20
- package/dist/bridge-route.d.ts +3 -0
- package/dist/bridge-route.js +22 -0
- package/dist/bridge.d.ts +7 -1
- package/dist/bridge.js +1 -0
- package/dist/data.d.ts +16 -0
- package/dist/data.js +59 -0
- package/dist/experimental.d.ts +10 -0
- package/dist/experimental.js +57 -0
- package/dist/index.d.ts +36 -1
- package/dist/index.js +87 -2
- package/dist/integration.d.ts +25 -0
- package/dist/integration.js +22 -0
- package/dist/internal/escape.d.ts +8 -0
- package/dist/internal/forms-store.d.ts +23 -0
- package/dist/internal/request-context.d.ts +16 -0
- package/dist/is-edit-mode.d.ts +32 -0
- package/dist/is-edit-mode.js +37 -0
- package/dist/island-route.d.ts +43 -0
- package/dist/island-route.js +57 -0
- package/dist/middleware.d.ts +20 -0
- package/dist/middleware.js +109 -0
- package/dist/sanitize.d.ts +12 -1
- package/dist/sanitize.js +6 -10
- package/dist/tina-field.d.ts +1 -1
- package/dist/tina-field.js +1 -0
- package/dist/types.d.ts +92 -1
- package/dist/types.js +0 -1
- package/package.json +89 -17
- package/src/CodeBlockNode.astro +28 -0
- package/src/Container.astro +56 -0
- package/src/ImageNode.astro +17 -0
- package/src/LinkNode.astro +22 -0
- package/src/MdxNode.astro +24 -0
- package/src/Node.astro +11 -4
- package/src/TinaIsland.astro +42 -0
- package/src/TinaMarkdown.astro +8 -0
- package/src/__tests__/TinaMarkdown.test.ts +112 -0
- package/src/__tests__/__snapshots__/TinaMarkdown.test.ts.snap +7 -0
- package/src/__tests__/fixtures/FancyHeading.astro +3 -0
- package/src/__tests__/fixtures/MyFeature.astro +4 -0
- package/src/__tests__/fixtures/basic-kitchen-sink.json +60 -0
- package/src/__tests__/fixtures/code-block.json +34 -0
- package/src/__tests__/fixtures/leaf-marks.json +199 -0
- package/src/__tests__/fixtures/mdx-jsx-flow.json +40 -0
- package/src/__tests__/fixtures/mdx-jsx-text.json +53 -0
- package/src/__tests__/sanitize.test.ts +75 -0
- package/src/bridge-route.ts +33 -0
- package/src/bridge.ts +7 -0
- package/src/data.ts +97 -0
- package/src/experimental.ts +14 -0
- package/src/index.ts +54 -0
- package/src/integration.ts +49 -0
- package/src/internal/escape.ts +15 -0
- package/src/internal/forms-store.ts +41 -0
- package/src/internal/request-context.ts +23 -0
- package/src/is-edit-mode.ts +68 -0
- package/src/island-route.ts +110 -0
- package/src/middleware.ts +118 -0
- package/src/sanitize.ts +64 -0
- package/src/tina-field.ts +1 -0
- package/src/types.ts +97 -0
- package/dist/preview.d.ts +0 -1
- package/dist/preview.js +0 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/island-route.ts
|
|
2
|
+
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
|
3
|
+
import { PREVIEW_CONTENT_TYPE } from "@tinacms/bridge/preview";
|
|
4
|
+
|
|
5
|
+
// src/internal/escape.ts
|
|
6
|
+
function escapeAttr(s) {
|
|
7
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/island-route.ts
|
|
11
|
+
function experimental_createIslandRoute(islands) {
|
|
12
|
+
return async ({ params, request, url }) => {
|
|
13
|
+
const rejection = rejectIfUnsafe(request);
|
|
14
|
+
if (rejection) return rejection;
|
|
15
|
+
const island = islands[params.name ?? ""];
|
|
16
|
+
if (!island) {
|
|
17
|
+
return new Response(`Unknown island "${params.name}"`, { status: 404 });
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const data = await island.fetch(request, url.searchParams);
|
|
21
|
+
const container = await AstroContainer.create();
|
|
22
|
+
const html = await container.renderToString(island.component, {
|
|
23
|
+
props: island.propsFromData(data, url.searchParams)
|
|
24
|
+
});
|
|
25
|
+
return new Response(wrapIsland(html, island.wrapper, url), {
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
28
|
+
"Cache-Control": "no-store"
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
return new Response("Island render failed", { status: 500 });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function rejectIfUnsafe(request) {
|
|
37
|
+
if (request.method !== "POST") {
|
|
38
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
39
|
+
}
|
|
40
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
41
|
+
if (!contentType.includes(PREVIEW_CONTENT_TYPE)) {
|
|
42
|
+
return new Response("Not Found", { status: 404 });
|
|
43
|
+
}
|
|
44
|
+
const fetchSite = request.headers.get("sec-fetch-site");
|
|
45
|
+
if (fetchSite === "cross-site" || fetchSite === "cross-origin") {
|
|
46
|
+
return new Response("Forbidden", { status: 403 });
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function wrapIsland(html, wrapper, url) {
|
|
51
|
+
const cls = wrapper.className ? ` class="${escapeAttr(wrapper.className)}"` : "";
|
|
52
|
+
const marker = escapeAttr(`${url.pathname}${url.search}`);
|
|
53
|
+
return `<${wrapper.tag}${cls} data-tina-island="${marker}">${html}</${wrapper.tag}>`;
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
experimental_createIslandRoute
|
|
57
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Astro middleware injected by the `tina()` integration.
|
|
3
|
+
*
|
|
4
|
+
* - Resolves `isEditMode(request)` once and stashes it on
|
|
5
|
+
* `context.locals.tinaEdit` so pages and components can branch on edit
|
|
6
|
+
* context without re-parsing headers.
|
|
7
|
+
* - Scopes the request and a per-request forms collector via
|
|
8
|
+
* AsyncLocalStorage so `tina()` reads them implicitly — the caller
|
|
9
|
+
* never threads `Astro.request` through their loaders.
|
|
10
|
+
* - In edit mode only, splices `<div data-tina-form>` payloads and a
|
|
11
|
+
* `<script>` that loads `/_tina/bridge.js` before `</head>`. The user
|
|
12
|
+
* writes nothing in their layout, and production HTML is byte-
|
|
13
|
+
* identical to a Tina-free Astro app.
|
|
14
|
+
* - In edit mode only, refreshes the `__tina_edit` cookie so the session
|
|
15
|
+
* survives in-iframe link clicks (whose Referer is the previous
|
|
16
|
+
* preview page, not `/admin/`).
|
|
17
|
+
*/
|
|
18
|
+
import type { MiddlewareHandler } from 'astro';
|
|
19
|
+
export declare const onRequest: MiddlewareHandler;
|
|
20
|
+
export default onRequest;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/internal/escape.ts
|
|
2
|
+
function escapeAttr(s) {
|
|
3
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/internal/forms-store.ts
|
|
7
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
+
var STORE_KEY = Symbol.for("@tinacms/astro/forms-store");
|
|
9
|
+
var slot = globalThis;
|
|
10
|
+
var formsStore = slot[STORE_KEY] ??= new AsyncLocalStorage();
|
|
11
|
+
|
|
12
|
+
// src/internal/request-context.ts
|
|
13
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
14
|
+
var STORE_KEY2 = Symbol.for("@tinacms/astro/request-context");
|
|
15
|
+
var slot2 = globalThis;
|
|
16
|
+
var requestStore = slot2[STORE_KEY2] ??= new AsyncLocalStorage2();
|
|
17
|
+
|
|
18
|
+
// src/is-edit-mode.ts
|
|
19
|
+
var EDIT_COOKIE = "__tina_edit";
|
|
20
|
+
var EDIT_COOKIE_HEADER = `${EDIT_COOKIE}=1; Path=/; SameSite=Strict; Max-Age=3600`;
|
|
21
|
+
function isEditMode(request) {
|
|
22
|
+
const url = new URL(request.url);
|
|
23
|
+
if (url.searchParams.get("tina-edit") === "1") return true;
|
|
24
|
+
const dest = request.headers.get("Sec-Fetch-Dest");
|
|
25
|
+
if (dest !== "iframe") return false;
|
|
26
|
+
const referer = request.headers.get("Referer");
|
|
27
|
+
if (referer) {
|
|
28
|
+
try {
|
|
29
|
+
const refererUrl = new URL(referer);
|
|
30
|
+
if (refererUrl.origin === url.origin && refererUrl.pathname.startsWith("/admin/")) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return readCookie(request, EDIT_COOKIE) === "1";
|
|
37
|
+
}
|
|
38
|
+
function readCookie(request, name) {
|
|
39
|
+
const header = request.headers.get("Cookie");
|
|
40
|
+
if (!header) return null;
|
|
41
|
+
for (const pair of header.split(";")) {
|
|
42
|
+
const eq = pair.indexOf("=");
|
|
43
|
+
if (eq === -1) continue;
|
|
44
|
+
const key = pair.slice(0, eq).trim();
|
|
45
|
+
if (key === name) return pair.slice(eq + 1).trim();
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/middleware.ts
|
|
51
|
+
var HEAD_CLOSE = "</head>";
|
|
52
|
+
var onRequest = (context, next) => {
|
|
53
|
+
const editing = isEditMode(context.request);
|
|
54
|
+
context.locals.tinaEdit = editing;
|
|
55
|
+
const forms = [];
|
|
56
|
+
return requestStore.run(
|
|
57
|
+
context.request,
|
|
58
|
+
() => formsStore.run(forms, async () => {
|
|
59
|
+
const response = await next();
|
|
60
|
+
return editing ? injectEditMode(response, forms) : response;
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
var middleware_default = onRequest;
|
|
65
|
+
async function injectEditMode(response, forms) {
|
|
66
|
+
const init = editModeInit(response);
|
|
67
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
68
|
+
if (!contentType.includes("text/html")) {
|
|
69
|
+
return new Response(response.body, init);
|
|
70
|
+
}
|
|
71
|
+
const html = await response.text();
|
|
72
|
+
const headEnd = html.indexOf(HEAD_CLOSE);
|
|
73
|
+
if (headEnd === -1) {
|
|
74
|
+
return new Response(html, init);
|
|
75
|
+
}
|
|
76
|
+
const injection = renderInjection(forms);
|
|
77
|
+
return new Response(
|
|
78
|
+
html.slice(0, headEnd) + injection + html.slice(headEnd),
|
|
79
|
+
init
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function editModeInit(response) {
|
|
83
|
+
const headers = new Headers(response.headers);
|
|
84
|
+
headers.delete("content-length");
|
|
85
|
+
headers.append("Set-Cookie", EDIT_COOKIE_HEADER);
|
|
86
|
+
return { status: response.status, statusText: response.statusText, headers };
|
|
87
|
+
}
|
|
88
|
+
function renderInjection(forms) {
|
|
89
|
+
const formDivs = forms.map(
|
|
90
|
+
(form) => `<div data-tina-form="${escapeAttr(JSON.stringify(form))}" hidden></div>`
|
|
91
|
+
).join("");
|
|
92
|
+
return formDivs + bridgeScript();
|
|
93
|
+
}
|
|
94
|
+
function bridgeScript() {
|
|
95
|
+
const origins = adminOrigins();
|
|
96
|
+
const initArg = origins ? `{adminOrigin:${JSON.stringify(origins)}}` : "";
|
|
97
|
+
return `<script type="module">import{init,refreshForms}from"/_tina/bridge.js";init(${initArg});document.addEventListener("astro:page-load",refreshForms);</script>`;
|
|
98
|
+
}
|
|
99
|
+
function adminOrigins() {
|
|
100
|
+
const env = import.meta.env;
|
|
101
|
+
const raw = env?.PUBLIC_TINA_ADMIN_ORIGIN;
|
|
102
|
+
if (!raw) return null;
|
|
103
|
+
const origins = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
104
|
+
return origins.length > 0 ? origins : null;
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
middleware_default as default,
|
|
108
|
+
onRequest
|
|
109
|
+
};
|
package/dist/sanitize.d.ts
CHANGED
|
@@ -1 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizes a CMS-supplied href, returning a safe URL or the fallback.
|
|
3
|
+
* Blocks dangerous schemes (javascript:, data:, vbscript:) and
|
|
4
|
+
* protocol-relative URLs (//evil.com). Allows relative paths, http(s),
|
|
5
|
+
* and mailto:.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeHref(value: unknown, fallback?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Validates a CMS-supplied image src, returning the src string if safe or ''
|
|
10
|
+
* if it is empty, not a string, or uses a non-http(s)/relative scheme.
|
|
11
|
+
*/
|
|
12
|
+
export declare function sanitizeImageSrc(src: unknown): string;
|
package/dist/sanitize.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
// src/sanitize.ts
|
|
1
2
|
function sanitizeHref(value, fallback = "#") {
|
|
2
|
-
if (typeof value !== "string")
|
|
3
|
-
return fallback;
|
|
3
|
+
if (typeof value !== "string") return fallback;
|
|
4
4
|
const trimmed = value.trim();
|
|
5
|
-
if (!trimmed)
|
|
6
|
-
return fallback;
|
|
5
|
+
if (!trimmed) return fallback;
|
|
7
6
|
const lower = trimmed.toLowerCase();
|
|
8
7
|
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:")) {
|
|
9
8
|
return fallback;
|
|
@@ -22,18 +21,15 @@ function sanitizeHref(value, fallback = "#") {
|
|
|
22
21
|
return fallback;
|
|
23
22
|
}
|
|
24
23
|
function sanitizeImageSrc(src) {
|
|
25
|
-
if (typeof src !== "string")
|
|
26
|
-
return "";
|
|
24
|
+
if (typeof src !== "string") return "";
|
|
27
25
|
const trimmed = src.trim();
|
|
28
|
-
if (!trimmed)
|
|
29
|
-
return "";
|
|
26
|
+
if (!trimmed) return "";
|
|
30
27
|
if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/") && !trimmed.startsWith("//")) {
|
|
31
28
|
return trimmed;
|
|
32
29
|
}
|
|
33
30
|
try {
|
|
34
31
|
const url = new URL(trimmed);
|
|
35
|
-
if (url.protocol === "http:" || url.protocol === "https:")
|
|
36
|
-
return trimmed;
|
|
32
|
+
if (url.protocol === "http:" || url.protocol === "https:") return trimmed;
|
|
37
33
|
} catch {
|
|
38
34
|
return "";
|
|
39
35
|
}
|
package/dist/tina-field.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from '@tinacms/bridge/tina-field';
|
package/dist/tina-field.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1 +1,92 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Plate AST node types Tina returns from rich-text fields. Mirrors the shape
|
|
3
|
+
* documented in `packages/@tinacms/mdx/src/parse/plate.ts` — kept inline so
|
|
4
|
+
* the Astro renderer can stay framework-free and not pull @tinacms/mdx into
|
|
5
|
+
* the page bundle.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Astro doesn't publicly export `AstroComponentFactory`, so we use a
|
|
9
|
+
* structural placeholder. The consumer's Astro pipeline performs the real
|
|
10
|
+
* compile-time check that whatever they pass is a valid component.
|
|
11
|
+
*/
|
|
12
|
+
export type AstroComponent = (...args: never[]) => unknown;
|
|
13
|
+
export type TinaRichTextRoot = {
|
|
14
|
+
type: 'root';
|
|
15
|
+
children: TinaRichTextNode[];
|
|
16
|
+
};
|
|
17
|
+
export type TinaRichTextNode = BlockElement | InlineElement | TextElement | MdxElement;
|
|
18
|
+
export type LinkElement = {
|
|
19
|
+
type: 'a';
|
|
20
|
+
url: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
children: InlineElement[];
|
|
23
|
+
};
|
|
24
|
+
export type ImageElement = {
|
|
25
|
+
type: 'img';
|
|
26
|
+
url: string;
|
|
27
|
+
alt?: string;
|
|
28
|
+
caption?: string;
|
|
29
|
+
};
|
|
30
|
+
export type CodeBlockElement = {
|
|
31
|
+
type: 'code_block';
|
|
32
|
+
lang?: string;
|
|
33
|
+
value?: string;
|
|
34
|
+
children?: {
|
|
35
|
+
children: TextElement[];
|
|
36
|
+
}[];
|
|
37
|
+
};
|
|
38
|
+
export type BlockElement = {
|
|
39
|
+
type: 'p';
|
|
40
|
+
children: InlineElement[];
|
|
41
|
+
} | {
|
|
42
|
+
type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
43
|
+
children: InlineElement[];
|
|
44
|
+
} | {
|
|
45
|
+
type: 'blockquote';
|
|
46
|
+
children: TinaRichTextNode[];
|
|
47
|
+
} | {
|
|
48
|
+
type: 'ul' | 'ol';
|
|
49
|
+
children: TinaRichTextNode[];
|
|
50
|
+
} | {
|
|
51
|
+
type: 'li';
|
|
52
|
+
children: TinaRichTextNode[];
|
|
53
|
+
} | {
|
|
54
|
+
type: 'lic';
|
|
55
|
+
children: InlineElement[];
|
|
56
|
+
} | {
|
|
57
|
+
type: 'hr';
|
|
58
|
+
} | {
|
|
59
|
+
type: 'break';
|
|
60
|
+
} | ImageElement | CodeBlockElement | {
|
|
61
|
+
type: 'maybe_mdx';
|
|
62
|
+
} | {
|
|
63
|
+
type: 'html';
|
|
64
|
+
value: string;
|
|
65
|
+
} | {
|
|
66
|
+
type: 'invalid_markdown';
|
|
67
|
+
value: string;
|
|
68
|
+
};
|
|
69
|
+
export type InlineElement = TextElement | LinkElement | {
|
|
70
|
+
type: 'html_inline';
|
|
71
|
+
value: string;
|
|
72
|
+
} | MdxElement;
|
|
73
|
+
export type TextElement = {
|
|
74
|
+
type: 'text';
|
|
75
|
+
text: string;
|
|
76
|
+
bold?: boolean;
|
|
77
|
+
italic?: boolean;
|
|
78
|
+
underline?: boolean;
|
|
79
|
+
strikethrough?: boolean;
|
|
80
|
+
code?: boolean;
|
|
81
|
+
highlight?: boolean;
|
|
82
|
+
highlightColor?: string;
|
|
83
|
+
};
|
|
84
|
+
export type MdxElement = {
|
|
85
|
+
type: 'mdxJsxFlowElement' | 'mdxJsxTextElement';
|
|
86
|
+
name: string;
|
|
87
|
+
props: Record<string, unknown>;
|
|
88
|
+
children?: TinaRichTextNode[];
|
|
89
|
+
};
|
|
90
|
+
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>;
|
package/dist/types.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
package/package.json
CHANGED
|
@@ -1,56 +1,128 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinacms/astro",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/TinaMarkdown.astro",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"package.json",
|
|
9
|
-
"
|
|
10
|
-
"src/TinaMarkdown.astro",
|
|
11
|
-
"src/Node.astro",
|
|
12
|
-
"src/Leaf.astro",
|
|
9
|
+
"src",
|
|
13
10
|
"dist"
|
|
14
11
|
],
|
|
15
12
|
"exports": {
|
|
16
13
|
".": {
|
|
17
14
|
"types": "./dist/index.d.ts",
|
|
18
|
-
"
|
|
15
|
+
"astro": "./src/TinaMarkdown.astro",
|
|
16
|
+
"default": "./dist/index.js"
|
|
19
17
|
},
|
|
18
|
+
"./TinaMarkdown.astro": "./src/TinaMarkdown.astro",
|
|
19
|
+
"./TinaIsland.astro": "./src/TinaIsland.astro",
|
|
20
20
|
"./types": {
|
|
21
21
|
"types": "./dist/types.d.ts",
|
|
22
|
+
"astro": "./src/types.ts",
|
|
22
23
|
"default": "./dist/types.js"
|
|
23
24
|
},
|
|
24
25
|
"./sanitize": {
|
|
25
26
|
"types": "./dist/sanitize.d.ts",
|
|
27
|
+
"astro": "./src/sanitize.ts",
|
|
26
28
|
"default": "./dist/sanitize.js"
|
|
27
29
|
},
|
|
28
30
|
"./bridge": {
|
|
29
31
|
"types": "./dist/bridge.d.ts",
|
|
32
|
+
"astro": "./src/bridge.ts",
|
|
30
33
|
"default": "./dist/bridge.js"
|
|
31
34
|
},
|
|
32
35
|
"./tina-field": {
|
|
33
36
|
"types": "./dist/tina-field.d.ts",
|
|
37
|
+
"astro": "./src/tina-field.ts",
|
|
34
38
|
"default": "./dist/tina-field.js"
|
|
35
39
|
},
|
|
36
|
-
"./
|
|
37
|
-
"types": "./dist/
|
|
38
|
-
"
|
|
40
|
+
"./data": {
|
|
41
|
+
"types": "./dist/data.d.ts",
|
|
42
|
+
"astro": "./src/data.ts",
|
|
43
|
+
"default": "./dist/data.js"
|
|
44
|
+
},
|
|
45
|
+
"./is-edit-mode": {
|
|
46
|
+
"types": "./dist/is-edit-mode.d.ts",
|
|
47
|
+
"astro": "./src/is-edit-mode.ts",
|
|
48
|
+
"default": "./dist/is-edit-mode.js"
|
|
49
|
+
},
|
|
50
|
+
"./middleware": {
|
|
51
|
+
"types": "./dist/middleware.d.ts",
|
|
52
|
+
"astro": "./src/middleware.ts",
|
|
53
|
+
"default": "./dist/middleware.js"
|
|
54
|
+
},
|
|
55
|
+
"./integration": {
|
|
56
|
+
"types": "./dist/integration.d.ts",
|
|
57
|
+
"astro": "./src/integration.ts",
|
|
58
|
+
"default": "./dist/integration.js"
|
|
59
|
+
},
|
|
60
|
+
"./bridge-route": {
|
|
61
|
+
"types": "./dist/bridge-route.d.ts",
|
|
62
|
+
"astro": "./src/bridge-route.ts",
|
|
63
|
+
"default": "./dist/bridge-route.js"
|
|
64
|
+
},
|
|
65
|
+
"./experimental": {
|
|
66
|
+
"types": "./dist/experimental.d.ts",
|
|
67
|
+
"astro": "./src/experimental.ts",
|
|
68
|
+
"default": "./dist/experimental.js"
|
|
39
69
|
}
|
|
40
70
|
},
|
|
41
71
|
"license": "Apache-2.0",
|
|
42
72
|
"buildConfig": {
|
|
43
73
|
"entryPoints": [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
{
|
|
75
|
+
"name": "src/index.ts",
|
|
76
|
+
"target": "node"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "src/types.ts",
|
|
80
|
+
"target": "node"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "src/sanitize.ts",
|
|
84
|
+
"target": "node"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "src/bridge.ts",
|
|
88
|
+
"target": "node"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "src/tina-field.ts",
|
|
92
|
+
"target": "node"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "src/data.ts",
|
|
96
|
+
"target": "node"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "src/is-edit-mode.ts",
|
|
100
|
+
"target": "node"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "src/middleware.ts",
|
|
104
|
+
"target": "node"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "src/integration.ts",
|
|
108
|
+
"target": "node"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "src/bridge-route.ts",
|
|
112
|
+
"target": "node"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "src/experimental.ts",
|
|
116
|
+
"target": "node"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "src/island-route.ts",
|
|
120
|
+
"target": "node"
|
|
121
|
+
}
|
|
50
122
|
]
|
|
51
123
|
},
|
|
52
124
|
"dependencies": {
|
|
53
|
-
"@tinacms/bridge": "0.0
|
|
125
|
+
"@tinacms/bridge": "0.2.0"
|
|
54
126
|
},
|
|
55
127
|
"peerDependencies": {
|
|
56
128
|
"astro": "^5.0.0"
|
|
@@ -61,7 +133,7 @@
|
|
|
61
133
|
"typescript": "^5.7.3",
|
|
62
134
|
"vite": "^6.0.0",
|
|
63
135
|
"vitest": "^3.0.0",
|
|
64
|
-
"@tinacms/scripts": "1.6.
|
|
136
|
+
"@tinacms/scripts": "1.6.1"
|
|
65
137
|
},
|
|
66
138
|
"publishConfig": {
|
|
67
139
|
"registry": "https://registry.npmjs.org"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* `code_block` nodes carry their value as either a flat `value` string or as
|
|
4
|
+
* `children: [{ children: [{ text }, ...] }, ...]` (one outer entry per line).
|
|
5
|
+
* Flatten both shapes to a single string before rendering.
|
|
6
|
+
*/
|
|
7
|
+
import type { CodeBlockElement, CustomComponentsMap } from './types';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
node: CodeBlockElement;
|
|
11
|
+
components: CustomComponentsMap;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { node, components } = Astro.props;
|
|
15
|
+
const Override = components.code_block;
|
|
16
|
+
|
|
17
|
+
const value =
|
|
18
|
+
node.value ??
|
|
19
|
+
node.children
|
|
20
|
+
?.map((line) => line.children?.map((tn) => tn.text).join('') ?? '')
|
|
21
|
+
.join('\n') ??
|
|
22
|
+
'';
|
|
23
|
+
---
|
|
24
|
+
{Override ? (
|
|
25
|
+
<Override value={value} lang={node.lang} />
|
|
26
|
+
) : (
|
|
27
|
+
<pre><code class={node.lang ? `language-${node.lang}` : undefined}>{value}</code></pre>
|
|
28
|
+
)}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders any node whose default form is `<Tag>{children}</Tag>` —
|
|
4
|
+
* paragraphs, headings, lists, list items, blockquote, lic. The tag itself
|
|
5
|
+
* is derived from `node.type`; if the consumer registered an override on
|
|
6
|
+
* `components[type]`, we render that instead and pass children as a slot.
|
|
7
|
+
*/
|
|
8
|
+
import TinaMarkdown from './TinaMarkdown.astro';
|
|
9
|
+
import type { CustomComponentsMap, TinaRichTextNode } from './types';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
node: TinaRichTextNode & { children: TinaRichTextNode[] };
|
|
13
|
+
components: CustomComponentsMap;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { node, components } = Astro.props;
|
|
17
|
+
const t = node.type;
|
|
18
|
+
const Override = components[t];
|
|
19
|
+
|
|
20
|
+
const tagFor: Record<
|
|
21
|
+
string,
|
|
22
|
+
| 'p'
|
|
23
|
+
| 'h1'
|
|
24
|
+
| 'h2'
|
|
25
|
+
| 'h3'
|
|
26
|
+
| 'h4'
|
|
27
|
+
| 'h5'
|
|
28
|
+
| 'h6'
|
|
29
|
+
| 'ol'
|
|
30
|
+
| 'ul'
|
|
31
|
+
| 'li'
|
|
32
|
+
| 'div'
|
|
33
|
+
| 'blockquote'
|
|
34
|
+
> = {
|
|
35
|
+
p: 'p',
|
|
36
|
+
h1: 'h1',
|
|
37
|
+
h2: 'h2',
|
|
38
|
+
h3: 'h3',
|
|
39
|
+
h4: 'h4',
|
|
40
|
+
h5: 'h5',
|
|
41
|
+
h6: 'h6',
|
|
42
|
+
ol: 'ol',
|
|
43
|
+
ul: 'ul',
|
|
44
|
+
li: 'li',
|
|
45
|
+
blockquote: 'blockquote',
|
|
46
|
+
lic: 'div',
|
|
47
|
+
};
|
|
48
|
+
const Tag = tagFor[t];
|
|
49
|
+
---
|
|
50
|
+
{Override ? (
|
|
51
|
+
<Override>
|
|
52
|
+
<TinaMarkdown content={node.children} components={components} />
|
|
53
|
+
</Override>
|
|
54
|
+
) : (
|
|
55
|
+
<Tag><TinaMarkdown content={node.children} components={components} /></Tag>
|
|
56
|
+
)}
|
|
@@ -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
|
+
)}
|