vestrycms 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +383 -2
- package/dist/config/fields.d.ts +30 -0
- package/dist/config/fields.js +75 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +18 -0
- package/dist/config/types.d.ts +412 -0
- package/dist/config/types.js +10 -0
- package/dist/core/api.d.ts +67 -0
- package/dist/core/api.js +543 -0
- package/dist/core/auth.d.ts +1597 -0
- package/dist/core/auth.js +264 -0
- package/dist/core/cache.d.ts +27 -0
- package/dist/core/cache.js +52 -0
- package/dist/core/entries.d.ts +19 -0
- package/dist/core/entries.js +127 -0
- package/dist/core/index-types.d.ts +20 -0
- package/dist/core/index-types.js +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/mail.d.ts +28 -0
- package/dist/core/mail.js +62 -0
- package/dist/core/paths.d.ts +35 -0
- package/dist/core/paths.js +148 -0
- package/dist/core/roles.d.ts +24 -0
- package/dist/core/roles.js +15 -0
- package/dist/core/service.d.ts +102 -0
- package/dist/core/service.js +478 -0
- package/dist/core/source.d.ts +82 -0
- package/dist/core/source.js +75 -0
- package/dist/core/sources/github-app.d.ts +26 -0
- package/dist/core/sources/github-app.js +161 -0
- package/dist/core/sources/github.d.ts +40 -0
- package/dist/core/sources/github.js +278 -0
- package/dist/core/sources/local.d.ts +16 -0
- package/dist/core/sources/local.js +98 -0
- package/dist/core/values.d.ts +22 -0
- package/dist/core/values.js +123 -0
- package/dist/env.d.ts +19 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +167 -0
- package/dist/integration/build-index.d.ts +8 -0
- package/dist/integration/build-index.js +67 -0
- package/dist/integration/client-deps.d.ts +10 -0
- package/dist/integration/client-deps.js +43 -0
- package/dist/integration/dev-middleware.d.ts +6 -0
- package/dist/integration/dev-middleware.js +48 -0
- package/dist/integration/formatter.d.ts +7 -0
- package/dist/integration/formatter.js +56 -0
- package/dist/mdx/expressions.d.ts +14 -0
- package/dist/mdx/expressions.js +141 -0
- package/dist/mdx/index.d.ts +4 -0
- package/dist/mdx/index.js +4 -0
- package/dist/mdx/parse.d.ts +6 -0
- package/dist/mdx/parse.js +401 -0
- package/dist/mdx/serialize.d.ts +9 -0
- package/dist/mdx/serialize.js +485 -0
- package/dist/mdx/types.d.ts +42 -0
- package/dist/mdx/types.js +6 -0
- package/dist/pages/admin.astro +27 -0
- package/dist/pages/api.d.ts +3 -0
- package/dist/pages/api.js +31 -0
- package/dist/ui/App.d.ts +4 -0
- package/dist/ui/App.js +70 -0
- package/dist/ui/api.d.ts +83 -0
- package/dist/ui/api.js +97 -0
- package/dist/ui/auth-client.d.ts +27 -0
- package/dist/ui/auth-client.js +29 -0
- package/dist/ui/components/AssetPicker.d.ts +27 -0
- package/dist/ui/components/AssetPicker.js +97 -0
- package/dist/ui/components/Icon.d.ts +11 -0
- package/dist/ui/components/Icon.js +46 -0
- package/dist/ui/components/Layout.d.ts +15 -0
- package/dist/ui/components/Layout.js +87 -0
- package/dist/ui/components/editor/EditorContext.d.ts +15 -0
- package/dist/ui/components/editor/EditorContext.js +9 -0
- package/dist/ui/components/editor/NodeViews.d.ts +4 -0
- package/dist/ui/components/editor/NodeViews.js +127 -0
- package/dist/ui/components/editor/RichTextEditor.d.ts +24 -0
- package/dist/ui/components/editor/RichTextEditor.js +230 -0
- package/dist/ui/components/editor/extensions.d.ts +30 -0
- package/dist/ui/components/editor/extensions.js +248 -0
- package/dist/ui/components/editor/schema.d.ts +12 -0
- package/dist/ui/components/editor/schema.js +37 -0
- package/dist/ui/components/fields/CodeInput.d.ts +10 -0
- package/dist/ui/components/fields/CodeInput.js +8 -0
- package/dist/ui/components/fields/CodeMirrorInput.d.ts +2 -0
- package/dist/ui/components/fields/CodeMirrorInput.js +44 -0
- package/dist/ui/components/fields/FieldRenderer.d.ts +41 -0
- package/dist/ui/components/fields/FieldRenderer.js +299 -0
- package/dist/ui/components/fields/ImageTools.d.ts +31 -0
- package/dist/ui/components/fields/ImageTools.js +296 -0
- package/dist/ui/components/fields/MarkdownField.d.ts +7 -0
- package/dist/ui/components/fields/MarkdownField.js +16 -0
- package/dist/ui/components/fields/TreeInput.d.ts +13 -0
- package/dist/ui/components/fields/TreeInput.js +221 -0
- package/dist/ui/components/ui.d.ts +63 -0
- package/dist/ui/components/ui.js +113 -0
- package/dist/ui/context.d.ts +17 -0
- package/dist/ui/context.js +13 -0
- package/dist/ui/lib/format.d.ts +5 -0
- package/dist/ui/lib/format.js +11 -0
- package/dist/ui/lib/utils.d.ts +24 -0
- package/dist/ui/lib/utils.js +255 -0
- package/dist/ui/main.d.ts +1 -0
- package/dist/ui/main.js +11 -0
- package/dist/ui/pages/Assets.d.ts +1 -0
- package/dist/ui/pages/Assets.js +69 -0
- package/dist/ui/pages/CollectionList.d.ts +6 -0
- package/dist/ui/pages/CollectionList.js +115 -0
- package/dist/ui/pages/Dashboard.d.ts +1 -0
- package/dist/ui/pages/Dashboard.js +57 -0
- package/dist/ui/pages/EntryEditor.d.ts +3 -0
- package/dist/ui/pages/EntryEditor.js +313 -0
- package/dist/ui/pages/Login.d.ts +6 -0
- package/dist/ui/pages/Login.js +88 -0
- package/dist/ui/pages/Users.d.ts +1 -0
- package/dist/ui/pages/Users.js +102 -0
- package/dist/ui/styles.css +2554 -0
- package/package.json +123 -3
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { gfmToMarkdown } from "mdast-util-gfm";
|
|
2
|
+
import { mdxToMarkdown } from "mdast-util-mdx";
|
|
3
|
+
import { defaultHandlers, toMarkdown, } from "mdast-util-to-markdown";
|
|
4
|
+
import { isPlainString, valueToSource } from "./expressions.js";
|
|
5
|
+
/**
|
|
6
|
+
* Note every identifier-like word in a piece of JS source. Words inside
|
|
7
|
+
* strings or comments count too, which at worst keeps an unused import.
|
|
8
|
+
*/
|
|
9
|
+
function scan(source, used) {
|
|
10
|
+
for (const m of source.matchAll(IDENTIFIERS))
|
|
11
|
+
used.identifiers.add(m[0]);
|
|
12
|
+
}
|
|
13
|
+
const IDENTIFIER = "[\\p{ID_Start}$_][\\p{ID_Continue}$\\u200c\\u200d]*";
|
|
14
|
+
const IDENTIFIERS = new RegExp(IDENTIFIER, "gu");
|
|
15
|
+
const IS_IDENTIFIER = new RegExp(`^${IDENTIFIER}$`, "u");
|
|
16
|
+
export function serializeMdx(doc, options = {}) {
|
|
17
|
+
const isMdx = options.format !== "md";
|
|
18
|
+
const used = { components: new Set(), identifiers: new Set() };
|
|
19
|
+
const root = {
|
|
20
|
+
type: "root",
|
|
21
|
+
children: blocks(doc.content ?? [], used),
|
|
22
|
+
};
|
|
23
|
+
const imports = isMdx ? importStatements(used, options) : [];
|
|
24
|
+
if (imports.length) {
|
|
25
|
+
root.children.unshift({
|
|
26
|
+
type: "mdxjsEsm",
|
|
27
|
+
value: imports.join("\n"),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const mdx = mdxToMarkdown({ quote: '"' });
|
|
31
|
+
const libFlow = findHandler(mdx, "mdxJsxFlowElement");
|
|
32
|
+
let out = toMarkdown(root, {
|
|
33
|
+
extensions: isMdx ? [mdx, gfmToMarkdown()] : [gfmToMarkdown()],
|
|
34
|
+
bullet: "-",
|
|
35
|
+
bulletOther: "*",
|
|
36
|
+
emphasis: "_",
|
|
37
|
+
strong: "*",
|
|
38
|
+
fences: true,
|
|
39
|
+
rule: "-",
|
|
40
|
+
listItemIndent: "one",
|
|
41
|
+
resourceLink: true,
|
|
42
|
+
handlers: {
|
|
43
|
+
rawHtml: (node) => node.value,
|
|
44
|
+
rawInlineHtml: (node) => node.value,
|
|
45
|
+
...(libFlow ? { mdxJsxFlowElement: flowElementHandler(libFlow) } : {}),
|
|
46
|
+
list: listHandler(defaultHandlers.list),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// Place a blank line after the import block like Prettier/MDX conventions.
|
|
50
|
+
out = out.replace(/\s+$/, "");
|
|
51
|
+
return out ? `${out}\n` : "";
|
|
52
|
+
}
|
|
53
|
+
function findHandler(ext, name) {
|
|
54
|
+
const own = ext.handlers?.[name];
|
|
55
|
+
if (typeof own === "function")
|
|
56
|
+
return own;
|
|
57
|
+
for (const sub of ext.extensions ?? []) {
|
|
58
|
+
const found = findHandler(sub, name);
|
|
59
|
+
if (found)
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Block components with children are written with blank lines around the
|
|
66
|
+
* children and no indentation:
|
|
67
|
+
*
|
|
68
|
+
* <Article>
|
|
69
|
+
*
|
|
70
|
+
* ## Heading
|
|
71
|
+
*
|
|
72
|
+
* </Article>
|
|
73
|
+
*
|
|
74
|
+
* The library default indents children, which Prettier's MDX parser then
|
|
75
|
+
* reflows as JSX text.
|
|
76
|
+
*/
|
|
77
|
+
function flowElementHandler(lib) {
|
|
78
|
+
return (node, parent, state, info) => {
|
|
79
|
+
const el = node;
|
|
80
|
+
if (!el.children?.length)
|
|
81
|
+
return lib(node, parent, state, info);
|
|
82
|
+
const open = lib({ ...el, children: [] }, parent, state, info).replace(/\s*\/>$/, ">");
|
|
83
|
+
const children = state.containerFlow(el, info);
|
|
84
|
+
return `${open}\n\n${children}\n\n</${el.name ?? ""}>`;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A tight list is written without blank lines, but some neighbouring blocks
|
|
89
|
+
* need one to stay apart (two paragraphs would merge, kind 7 HTML would join
|
|
90
|
+
* the paragraph before it, ...), and a blank line anywhere in a list makes it
|
|
91
|
+
* loose when read back. Ask the serializer's own join rules whether any
|
|
92
|
+
* neighbours in the list or its items need a blank line, and if so write the
|
|
93
|
+
* whole list loose.
|
|
94
|
+
*/
|
|
95
|
+
function listHandler(lib) {
|
|
96
|
+
return (node, parent, state, info) => {
|
|
97
|
+
const list = node;
|
|
98
|
+
if (!list.spread &&
|
|
99
|
+
[list, ...list.children].some((p) => needsBlankLine(p, state))) {
|
|
100
|
+
list.spread = true;
|
|
101
|
+
for (const item of list.children)
|
|
102
|
+
item.spread = true;
|
|
103
|
+
}
|
|
104
|
+
return lib(node, parent, state, info);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Mirrors mdast-util-to-markdown's `between`: tight only if a join says 0. */
|
|
108
|
+
function needsBlankLine(parent, state) {
|
|
109
|
+
const children = parent.children;
|
|
110
|
+
for (let i = 1; i < children.length; i++) {
|
|
111
|
+
let index = state.join.length;
|
|
112
|
+
let lines;
|
|
113
|
+
while (index--) {
|
|
114
|
+
const result = state.join[index](children[i - 1], children[i], parent, state);
|
|
115
|
+
if (result === true || result === 1)
|
|
116
|
+
break;
|
|
117
|
+
if (typeof result === "number" || result === false) {
|
|
118
|
+
lines = result === false ? 1 : result;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (lines !== 0)
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
function importStatements(used, options) {
|
|
128
|
+
const lines = new Map();
|
|
129
|
+
const add = (name, spec) => {
|
|
130
|
+
if (spec)
|
|
131
|
+
lines.set(name, importLine(spec));
|
|
132
|
+
};
|
|
133
|
+
// Components get the registry's import, falling back to the source's.
|
|
134
|
+
for (const name of used.components) {
|
|
135
|
+
const reg = options.components?.[name];
|
|
136
|
+
add(name, reg?.import
|
|
137
|
+
? { name, from: reg.import, named: reg.namedImport }
|
|
138
|
+
: options.existingImports?.[name]);
|
|
139
|
+
}
|
|
140
|
+
// Anything else the source imported stays while it is still referenced.
|
|
141
|
+
for (const [name, spec] of Object.entries(options.existingImports ?? {}))
|
|
142
|
+
if (!lines.has(name) && used.identifiers.has(name))
|
|
143
|
+
add(name, spec);
|
|
144
|
+
const originalOrder = Object.keys(options.existingImports ?? {});
|
|
145
|
+
return [...lines.entries()]
|
|
146
|
+
.sort(([a], [b]) => {
|
|
147
|
+
const ia = originalOrder.indexOf(a);
|
|
148
|
+
const ib = originalOrder.indexOf(b);
|
|
149
|
+
if (ia !== -1 && ib !== -1)
|
|
150
|
+
return ia - ib;
|
|
151
|
+
if (ia !== -1)
|
|
152
|
+
return -1;
|
|
153
|
+
if (ib !== -1)
|
|
154
|
+
return 1;
|
|
155
|
+
return a.localeCompare(b);
|
|
156
|
+
})
|
|
157
|
+
.map(([, l]) => l);
|
|
158
|
+
}
|
|
159
|
+
function importLine(spec) {
|
|
160
|
+
const from = JSON.stringify(spec.from);
|
|
161
|
+
if (!spec.named)
|
|
162
|
+
return `import ${spec.name} from ${from};`;
|
|
163
|
+
const imported = spec.imported === undefined || spec.imported === spec.name
|
|
164
|
+
? ""
|
|
165
|
+
: IS_IDENTIFIER.test(spec.imported)
|
|
166
|
+
? `${spec.imported} as `
|
|
167
|
+
: `${JSON.stringify(spec.imported)} as `;
|
|
168
|
+
return `import { ${imported}${spec.name} } from ${from};`;
|
|
169
|
+
}
|
|
170
|
+
function blocks(nodes, used) {
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const node of nodes) {
|
|
173
|
+
const converted = block(node, used);
|
|
174
|
+
if (converted)
|
|
175
|
+
out.push(converted);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
function block(node, used) {
|
|
180
|
+
switch (node.type) {
|
|
181
|
+
case "paragraph": {
|
|
182
|
+
const children = inlines(node.content ?? [], used);
|
|
183
|
+
if (!children.length)
|
|
184
|
+
return null;
|
|
185
|
+
return { type: "paragraph", children: children };
|
|
186
|
+
}
|
|
187
|
+
case "heading":
|
|
188
|
+
return {
|
|
189
|
+
type: "heading",
|
|
190
|
+
depth: clampDepth(Number(node.attrs?.level ?? 1)),
|
|
191
|
+
children: inlines(node.content ?? [], used),
|
|
192
|
+
};
|
|
193
|
+
case "horizontalRule":
|
|
194
|
+
return { type: "thematicBreak" };
|
|
195
|
+
case "blockquote":
|
|
196
|
+
return {
|
|
197
|
+
type: "blockquote",
|
|
198
|
+
children: blocks(node.content ?? [], used),
|
|
199
|
+
};
|
|
200
|
+
case "bulletList":
|
|
201
|
+
case "orderedList": {
|
|
202
|
+
const ordered = node.type === "orderedList";
|
|
203
|
+
// Lists that must be loose to keep their content apart are loosened
|
|
204
|
+
// while serializing; see listHandler.
|
|
205
|
+
const spread = node.attrs?.spread === true;
|
|
206
|
+
const items = (node.content ?? []).map((item) => {
|
|
207
|
+
const checked = item.attrs?.checked;
|
|
208
|
+
return {
|
|
209
|
+
type: "listItem",
|
|
210
|
+
spread,
|
|
211
|
+
checked: typeof checked === "boolean" ? checked : null,
|
|
212
|
+
children: blocks(item.content ?? [], used),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
type: "list",
|
|
217
|
+
ordered,
|
|
218
|
+
start: ordered ? Number(node.attrs?.start ?? 1) : null,
|
|
219
|
+
spread,
|
|
220
|
+
children: items,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
case "codeBlock":
|
|
224
|
+
return {
|
|
225
|
+
type: "code",
|
|
226
|
+
lang: node.attrs?.language ?? null,
|
|
227
|
+
meta: node.attrs?.meta ?? null,
|
|
228
|
+
value: (node.content ?? []).map((t) => t.text ?? "").join(""),
|
|
229
|
+
};
|
|
230
|
+
case "rawHtml":
|
|
231
|
+
return { type: "html", value: String(node.attrs?.value ?? "") };
|
|
232
|
+
case "table":
|
|
233
|
+
return table(node, used);
|
|
234
|
+
case "mdxComponent":
|
|
235
|
+
return jsxElement(node, used, false);
|
|
236
|
+
case "footnoteDefinition":
|
|
237
|
+
return {
|
|
238
|
+
type: "footnoteDefinition",
|
|
239
|
+
...footnoteLabel(node),
|
|
240
|
+
children: blocks(node.content ?? [], used),
|
|
241
|
+
};
|
|
242
|
+
case "mdxExpression": {
|
|
243
|
+
const value = String(node.attrs?.value ?? "");
|
|
244
|
+
scan(value, used);
|
|
245
|
+
return { type: "mdxFlowExpression", value };
|
|
246
|
+
}
|
|
247
|
+
case "mdxEsm": {
|
|
248
|
+
const value = String(node.attrs?.value ?? "");
|
|
249
|
+
scan(value, used);
|
|
250
|
+
return { type: "mdxjsEsm", value };
|
|
251
|
+
}
|
|
252
|
+
case "image":
|
|
253
|
+
return {
|
|
254
|
+
type: "paragraph",
|
|
255
|
+
children: inlines([node], used),
|
|
256
|
+
};
|
|
257
|
+
default: {
|
|
258
|
+
const children = inlines([node], used);
|
|
259
|
+
return children.length
|
|
260
|
+
? { type: "paragraph", children: children }
|
|
261
|
+
: null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function footnoteLabel(node) {
|
|
266
|
+
const label = String(node.attrs?.label ?? node.attrs?.identifier ?? "");
|
|
267
|
+
const identifier = String(node.attrs?.identifier ?? label.toLowerCase());
|
|
268
|
+
return { identifier, label };
|
|
269
|
+
}
|
|
270
|
+
function clampDepth(n) {
|
|
271
|
+
return Math.min(6, Math.max(1, Math.round(n)));
|
|
272
|
+
}
|
|
273
|
+
function table(node, used) {
|
|
274
|
+
const rows = node.content ?? [];
|
|
275
|
+
const align = [];
|
|
276
|
+
const children = rows.map((row, rowIdx) => ({
|
|
277
|
+
type: "tableRow",
|
|
278
|
+
children: (row.content ?? []).map((cell, colIdx) => {
|
|
279
|
+
if (rowIdx === 0)
|
|
280
|
+
align[colIdx] = cell.attrs?.align ?? null;
|
|
281
|
+
const paragraphs = cell.content ?? [];
|
|
282
|
+
const inline = [];
|
|
283
|
+
paragraphs.forEach((p, i) => {
|
|
284
|
+
if (i > 0)
|
|
285
|
+
inline.push({ type: "hardBreak" });
|
|
286
|
+
inline.push(...(p.type === "paragraph" ? (p.content ?? []) : [p]));
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
type: "tableCell",
|
|
290
|
+
children: inlines(inline, used),
|
|
291
|
+
};
|
|
292
|
+
}),
|
|
293
|
+
}));
|
|
294
|
+
return { type: "table", align, children };
|
|
295
|
+
}
|
|
296
|
+
function propsToAttributes(node, used, inline = false) {
|
|
297
|
+
const props = (node.attrs?.props ?? {});
|
|
298
|
+
const spreads = (node.attrs?.spreads ?? []);
|
|
299
|
+
const attrs = [];
|
|
300
|
+
for (const [name, value] of Object.entries(props)) {
|
|
301
|
+
if (value === undefined)
|
|
302
|
+
continue;
|
|
303
|
+
if (value === true) {
|
|
304
|
+
attrs.push({ type: "mdxJsxAttribute", name, value: null });
|
|
305
|
+
}
|
|
306
|
+
else if (isPlainString(value)) {
|
|
307
|
+
attrs.push({ type: "mdxJsxAttribute", name, value });
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
// Block components print each attribute on its own line, indented
|
|
311
|
+
// two spaces, so nested lines are indented relative to that.
|
|
312
|
+
const source = valueToSource(value, inline ? "" : " ");
|
|
313
|
+
scan(source, used);
|
|
314
|
+
attrs.push({
|
|
315
|
+
type: "mdxJsxAttribute",
|
|
316
|
+
name,
|
|
317
|
+
value: { type: "mdxJsxAttributeValueExpression", value: source },
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const s of spreads) {
|
|
322
|
+
scan(s, used);
|
|
323
|
+
attrs.push({ type: "mdxJsxExpressionAttribute", value: s });
|
|
324
|
+
}
|
|
325
|
+
return attrs;
|
|
326
|
+
}
|
|
327
|
+
function jsxElement(node, used, inline) {
|
|
328
|
+
const name = String(node.attrs?.name ?? "");
|
|
329
|
+
if (name && /^[A-Z]/.test(name))
|
|
330
|
+
used.components.add(name);
|
|
331
|
+
// `<ui.Card />` needs the import of `ui`.
|
|
332
|
+
scan(name, used);
|
|
333
|
+
const attributes = propsToAttributes(node, used, inline);
|
|
334
|
+
if (inline) {
|
|
335
|
+
return {
|
|
336
|
+
type: "mdxJsxTextElement",
|
|
337
|
+
name: name || null,
|
|
338
|
+
attributes,
|
|
339
|
+
children: inlines(node.content ?? [], used),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
type: "mdxJsxFlowElement",
|
|
344
|
+
name: name || null,
|
|
345
|
+
attributes,
|
|
346
|
+
children: blocks(node.content ?? [], used),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const MARK_ORDER = [
|
|
350
|
+
"link",
|
|
351
|
+
"superscript",
|
|
352
|
+
"subscript",
|
|
353
|
+
"bold",
|
|
354
|
+
"italic",
|
|
355
|
+
"strike",
|
|
356
|
+
"code",
|
|
357
|
+
];
|
|
358
|
+
/**
|
|
359
|
+
* Convert a flat run of inline PM nodes (text with marks) into nested mdast
|
|
360
|
+
* phrasing content. At each step the mark covering the longest run of
|
|
361
|
+
* following nodes becomes the outer wrapper, so `**a <sup>6</sup> b**`
|
|
362
|
+
* keeps bold outermost.
|
|
363
|
+
*/
|
|
364
|
+
function inlines(nodes, used) {
|
|
365
|
+
return group(nodes, used);
|
|
366
|
+
}
|
|
367
|
+
function markKey(m) {
|
|
368
|
+
return JSON.stringify([m.type, m.attrs ?? {}]);
|
|
369
|
+
}
|
|
370
|
+
function hasMark(node, key) {
|
|
371
|
+
return (node.marks ?? []).some((m) => markKey(m) === key);
|
|
372
|
+
}
|
|
373
|
+
function stripMark(node, key) {
|
|
374
|
+
return {
|
|
375
|
+
...node,
|
|
376
|
+
marks: (node.marks ?? []).filter((m) => markKey(m) !== key),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function group(nodes, used) {
|
|
380
|
+
const out = [];
|
|
381
|
+
let i = 0;
|
|
382
|
+
while (i < nodes.length) {
|
|
383
|
+
const node = nodes[i];
|
|
384
|
+
const marks = node.marks ?? [];
|
|
385
|
+
if (!marks.length) {
|
|
386
|
+
out.push(...leaf(node, used));
|
|
387
|
+
i++;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
// Pick the mark with the longest run from here.
|
|
391
|
+
let best = null;
|
|
392
|
+
let bestLen = 0;
|
|
393
|
+
for (const m of marks) {
|
|
394
|
+
const key = markKey(m);
|
|
395
|
+
let j = i;
|
|
396
|
+
while (j < nodes.length && hasMark(nodes[j], key))
|
|
397
|
+
j++;
|
|
398
|
+
const len = j - i;
|
|
399
|
+
const better = len > bestLen ||
|
|
400
|
+
(len === bestLen &&
|
|
401
|
+
best !== null &&
|
|
402
|
+
MARK_ORDER.indexOf(m.type) <
|
|
403
|
+
MARK_ORDER.indexOf(best.type));
|
|
404
|
+
if (better) {
|
|
405
|
+
best = m;
|
|
406
|
+
bestLen = len;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const key = markKey(best);
|
|
410
|
+
const run = nodes.slice(i, i + bestLen).map((n) => stripMark(n, key));
|
|
411
|
+
const children = group(run, used);
|
|
412
|
+
out.push(wrap(best, children));
|
|
413
|
+
i += bestLen;
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
function wrap(mark, children) {
|
|
418
|
+
switch (mark.type) {
|
|
419
|
+
case "bold":
|
|
420
|
+
return { type: "strong", children };
|
|
421
|
+
case "italic":
|
|
422
|
+
return { type: "emphasis", children };
|
|
423
|
+
case "strike":
|
|
424
|
+
return { type: "delete", children };
|
|
425
|
+
case "link":
|
|
426
|
+
return {
|
|
427
|
+
type: "link",
|
|
428
|
+
url: String(mark.attrs?.href ?? ""),
|
|
429
|
+
title: mark.attrs?.title ?? null,
|
|
430
|
+
children,
|
|
431
|
+
};
|
|
432
|
+
case "superscript":
|
|
433
|
+
return {
|
|
434
|
+
type: "mdxJsxTextElement",
|
|
435
|
+
name: "sup",
|
|
436
|
+
attributes: [],
|
|
437
|
+
children,
|
|
438
|
+
};
|
|
439
|
+
case "subscript":
|
|
440
|
+
return {
|
|
441
|
+
type: "mdxJsxTextElement",
|
|
442
|
+
name: "sub",
|
|
443
|
+
attributes: [],
|
|
444
|
+
children,
|
|
445
|
+
};
|
|
446
|
+
case "code": {
|
|
447
|
+
const text = children.map((c) => ("value" in c ? c.value : "")).join("");
|
|
448
|
+
return { type: "inlineCode", value: text };
|
|
449
|
+
}
|
|
450
|
+
default:
|
|
451
|
+
return { type: "paragraph", children };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function leaf(node, used) {
|
|
455
|
+
switch (node.type) {
|
|
456
|
+
case "text":
|
|
457
|
+
return node.text ? [{ type: "text", value: node.text }] : [];
|
|
458
|
+
case "hardBreak":
|
|
459
|
+
return [{ type: "break" }];
|
|
460
|
+
case "image":
|
|
461
|
+
return [
|
|
462
|
+
{
|
|
463
|
+
type: "image",
|
|
464
|
+
url: String(node.attrs?.src ?? ""),
|
|
465
|
+
alt: node.attrs?.alt ?? null,
|
|
466
|
+
title: node.attrs?.title ?? null,
|
|
467
|
+
},
|
|
468
|
+
];
|
|
469
|
+
case "rawInlineHtml":
|
|
470
|
+
return [{ type: "html", value: String(node.attrs?.value ?? "") }];
|
|
471
|
+
case "mdxInlineComponent":
|
|
472
|
+
return [jsxElement(node, used, true)];
|
|
473
|
+
case "mdxInlineExpression": {
|
|
474
|
+
const value = String(node.attrs?.value ?? "");
|
|
475
|
+
scan(value, used);
|
|
476
|
+
return [{ type: "mdxTextExpression", value }];
|
|
477
|
+
}
|
|
478
|
+
case "footnoteReference":
|
|
479
|
+
return [{ type: "footnoteReference", ...footnoteLabel(node) }];
|
|
480
|
+
case "mdxComponent":
|
|
481
|
+
return [jsxElement(node, used, true)];
|
|
482
|
+
default:
|
|
483
|
+
return node.content ? inlines(node.content, used) : [];
|
|
484
|
+
}
|
|
485
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Minimal ProseMirror JSON shapes, shared by the converter and the editor. */
|
|
2
|
+
export interface PMMark {
|
|
3
|
+
type: string;
|
|
4
|
+
attrs?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface PMNode {
|
|
7
|
+
type: string;
|
|
8
|
+
attrs?: Record<string, unknown>;
|
|
9
|
+
content?: PMNode[];
|
|
10
|
+
marks?: PMMark[];
|
|
11
|
+
text?: string;
|
|
12
|
+
}
|
|
13
|
+
/** A non-literal JSX attribute value, kept verbatim as source. */
|
|
14
|
+
export interface RawExpression {
|
|
15
|
+
$raw: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function isRawExpression(v: unknown): v is RawExpression;
|
|
18
|
+
export type PropValue = unknown;
|
|
19
|
+
export interface ImportSpec {
|
|
20
|
+
/** Local identifier, e.g. `Highlights`. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Module specifier. */
|
|
23
|
+
from: string;
|
|
24
|
+
/** Default or named import. */
|
|
25
|
+
named?: boolean;
|
|
26
|
+
/** Exported name of a named import when it differs from `name` (`import { imported as name }`). */
|
|
27
|
+
imported?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ParsedDocument {
|
|
30
|
+
doc: PMNode;
|
|
31
|
+
/** Imports found in the source, keyed by local name. */
|
|
32
|
+
imports: Record<string, ImportSpec>;
|
|
33
|
+
}
|
|
34
|
+
export interface ComponentRegistry {
|
|
35
|
+
/** Component name → import spec to emit when it is used. */
|
|
36
|
+
[name: string]: {
|
|
37
|
+
import?: string;
|
|
38
|
+
namedImport?: boolean;
|
|
39
|
+
inline?: boolean;
|
|
40
|
+
children?: boolean | "text";
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
/// <reference path="../env.d.ts" />
|
|
3
|
+
import { basePath, name } from "virtual:vestry/runtime";
|
|
4
|
+
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!doctype html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8" />
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
13
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
14
|
+
<meta name="color-scheme" content="light" />
|
|
15
|
+
<title>{name}</title>
|
|
16
|
+
<link
|
|
17
|
+
rel="icon"
|
|
18
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%231e293b'/%3E%3Cpath d='M9 9l7 14 7-14' stroke='%23f8fafc' stroke-width='3.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"
|
|
19
|
+
/>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<div id="vestry-root" data-base={basePath} data-name={name}></div>
|
|
23
|
+
<script>
|
|
24
|
+
import "../ui/main.js";
|
|
25
|
+
</script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import config from "virtual:vestry/config";
|
|
2
|
+
import index from "virtual:vestry/index";
|
|
3
|
+
import { basePath } from "virtual:vestry/runtime";
|
|
4
|
+
import { createApi } from "../core/api.js";
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
function readEnv(key) {
|
|
7
|
+
// Cloudflare (nodejs_compat populates process.env), Node adapters, and
|
|
8
|
+
// anything else that exposes a process.env object.
|
|
9
|
+
const fromProcess = globalThis.process?.env?.[key];
|
|
10
|
+
if (fromProcess !== undefined)
|
|
11
|
+
return fromProcess;
|
|
12
|
+
const fromMeta = import.meta.env?.[key];
|
|
13
|
+
return fromMeta;
|
|
14
|
+
}
|
|
15
|
+
const api = createApi({ config, index, basePath, env: readEnv });
|
|
16
|
+
/**
|
|
17
|
+
* Cloudflare bindings (the D1 database) come from `cloudflare:workers`; the
|
|
18
|
+
* adapter's `locals.runtime.env` was removed in Astro 6 and now throws.
|
|
19
|
+
* Imported lazily and by variable so other runtimes just get no bindings.
|
|
20
|
+
*/
|
|
21
|
+
async function bindings() {
|
|
22
|
+
try {
|
|
23
|
+
const specifier = "cloudflare:workers";
|
|
24
|
+
const mod = (await import(/* @vite-ignore */ specifier));
|
|
25
|
+
return mod.env;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export const ALL = async ({ request }) => api.handle(request, { bindings: await bindings() });
|
package/dist/ui/App.d.ts
ADDED
package/dist/ui/App.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { QueryClient, QueryClientProvider, useQuery, } from "@tanstack/react-query";
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { BrowserRouter, Navigate, Route, Routes, useLocation, } from "react-router";
|
|
5
|
+
import { createClient, UNAUTHORIZED_EVENT } from "./api.js";
|
|
6
|
+
import { createVestryAuthClient } from "./auth-client.js";
|
|
7
|
+
import { Shell } from "./components/Layout.js";
|
|
8
|
+
import { Alert, Loading, ToastProvider } from "./components/ui.js";
|
|
9
|
+
import { VestryContext } from "./context.js";
|
|
10
|
+
import { AssetsPage } from "./pages/Assets.js";
|
|
11
|
+
import { CollectionListPage } from "./pages/CollectionList.js";
|
|
12
|
+
import { DashboardPage } from "./pages/Dashboard.js";
|
|
13
|
+
import { CollectionEntryPage, NewEntryPage, SingletonPage, } from "./pages/EntryEditor.js";
|
|
14
|
+
import { LoginPage } from "./pages/Login.js";
|
|
15
|
+
import { UsersPage } from "./pages/Users.js";
|
|
16
|
+
export function App({ basePath, name }) {
|
|
17
|
+
const queryClient = useMemo(() => new QueryClient({
|
|
18
|
+
defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
|
|
19
|
+
}), []);
|
|
20
|
+
return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ToastProvider, { children: _jsx(BrowserRouter, { basename: basePath, children: _jsx(Root, { basePath: basePath, name: name }) }) }) }));
|
|
21
|
+
}
|
|
22
|
+
function Root({ basePath, name }) {
|
|
23
|
+
const api = useMemo(() => createClient(basePath), [basePath]);
|
|
24
|
+
const authClient = useMemo(() => createVestryAuthClient(basePath), [basePath]);
|
|
25
|
+
const location = useLocation();
|
|
26
|
+
const configQuery = useQuery({
|
|
27
|
+
queryKey: ["config"],
|
|
28
|
+
queryFn: () => api.config(),
|
|
29
|
+
staleTime: Infinity,
|
|
30
|
+
});
|
|
31
|
+
// When a session expires mid-use the API answers 401; reloading the config
|
|
32
|
+
// drops the user and the router falls back to the sign-in page.
|
|
33
|
+
const refetch = configQuery.refetch;
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const onUnauthorized = () => void refetch();
|
|
36
|
+
window.addEventListener(UNAUTHORIZED_EVENT, onUnauthorized);
|
|
37
|
+
return () => window.removeEventListener(UNAUTHORIZED_EVENT, onUnauthorized);
|
|
38
|
+
}, [refetch]);
|
|
39
|
+
if (configQuery.isLoading)
|
|
40
|
+
return _jsx(Loading, { label: "Loading Vestry\u2026" });
|
|
41
|
+
if (configQuery.error || !configQuery.data) {
|
|
42
|
+
return (_jsx("div", { className: "v-page", children: _jsxs(Alert, { kind: "error", children: ["Could not load the Vestry configuration:", " ", configQuery.error?.message] }) }));
|
|
43
|
+
}
|
|
44
|
+
const client = configQuery.data;
|
|
45
|
+
const value = {
|
|
46
|
+
basePath,
|
|
47
|
+
name: client.config.name ?? name,
|
|
48
|
+
api,
|
|
49
|
+
authClient,
|
|
50
|
+
client,
|
|
51
|
+
config: client.config,
|
|
52
|
+
refresh: async () => {
|
|
53
|
+
await configQuery.refetch();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
if (!client.user) {
|
|
57
|
+
return (_jsx(VestryContext.Provider, { value: value, children: _jsxs(Routes, { children: [_jsx(Route, { path: "/login", element: _jsx(LoginPage, {}) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: `/login?next=${encodeURIComponent(basePath + location.pathname + location.search)}`, replace: true }) })] }) }));
|
|
58
|
+
}
|
|
59
|
+
return (_jsx(VestryContext.Provider, { value: value, children: _jsxs(Routes, { children: [_jsx(Route, { path: "/login", element: _jsx(Navigate, { to: afterLogin(location.search, basePath), replace: true }) }), _jsx(Route, { path: "*", element: _jsx(Shell, { children: _jsxs(Routes, { children: [_jsx(Route, { index: true, element: _jsx(DashboardPage, {}) }), _jsx(Route, { path: "collections/:collection", element: _jsx(CollectionListPage, {}) }), _jsx(Route, { path: "collections/:collection/new", element: _jsx(NewEntryPage, {}) }), _jsx(Route, { path: "collections/:collection/*", element: _jsx(CollectionEntryPage, {}) }), _jsx(Route, { path: "singletons/:singleton", element: _jsx(SingletonPage, {}) }), _jsx(Route, { path: "assets/:container", element: _jsx(AssetsPage, {}) }), _jsx(Route, { path: "users", element: _jsx(UsersPage, {}) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/", replace: true }) })] }) }) })] }) }));
|
|
60
|
+
}
|
|
61
|
+
/** Where to go after sign-in: the `next` the login page was sent with, made router-relative. */
|
|
62
|
+
function afterLogin(search, basePath) {
|
|
63
|
+
const next = new URLSearchParams(search).get("next");
|
|
64
|
+
if (!next || !next.startsWith("/") || next.startsWith("//"))
|
|
65
|
+
return "/";
|
|
66
|
+
const base = basePath.replace(/\/$/, "");
|
|
67
|
+
const underBase = next === base || next.startsWith(`${base}/`) || next.startsWith(`${base}?`);
|
|
68
|
+
const rest = underBase ? next.slice(base.length) : next;
|
|
69
|
+
return rest.startsWith("/") ? rest : `/${rest}`;
|
|
70
|
+
}
|