caracms 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 +57 -2
- package/bin/caracms.mjs +174 -0
- package/bin/gate.mjs +275 -0
- package/bin/init.mjs +712 -0
- package/bin/media.mjs +662 -0
- package/bin/site-runner.mjs +61 -0
- package/bin/templates/Page.astro.txt +31 -0
- package/bin/templates/TextBlock.astro.txt +13 -0
- package/bin/templates/caracms.config.ts.txt +22 -0
- package/bin/templates/catch-all.astro.txt +25 -0
- package/bin/templates/health.ts.txt +6 -0
- package/bin/templates/railway.ts.txt +147 -0
- package/package.json +66 -4
- package/skills/caracms-adopt/SKILL.md +508 -0
- package/skills/caracms-adopt/look-same.mjs +957 -0
- package/skills/caracms-blocks/SKILL.md +376 -0
- package/skills/caracms-content/SKILL.md +201 -0
- package/skills/caracms-tina/SKILL.md +427 -0
- package/skills/caracms-tina/field-kinds.md +88 -0
- package/skills/caracms-tina/tina-to-caracms.mjs +1011 -0
- package/src/block-runs.ts +84 -0
- package/src/cards.ts +158 -0
- package/src/cli/schemas.ts +122 -0
- package/src/cli/site.ts +66 -0
- package/src/cli/validate.ts +471 -0
- package/src/code-routes-disk.ts +67 -0
- package/src/code-routes.ts +134 -0
- package/src/columns.ts +39 -0
- package/src/components/Block.astro +46 -0
- package/src/components/BlockRunTree.astro +45 -0
- package/src/components/Blocks.astro +139 -0
- package/src/components/Buttons.astro +116 -0
- package/src/components/Fonts.astro +14 -0
- package/src/components/Form.astro +221 -0
- package/src/components/Head.astro +62 -0
- package/src/components/Link.astro +68 -0
- package/src/components/Lockup.astro +116 -0
- package/src/components/Media.astro +124 -0
- package/src/components/MediaPlayer.astro +102 -0
- package/src/components/RenderDocument.astro +64 -0
- package/src/components/RichText.astro +59 -0
- package/src/components/Text.astro +81 -0
- package/src/components/attributes.ts +33 -0
- package/src/components/descriptor.ts +45 -0
- package/src/components/form-client.ts +182 -0
- package/src/components/media-player.ts +46 -0
- package/src/components/media-props.ts +35 -0
- package/src/components/sitemap.xml.ts +36 -0
- package/src/config.ts +825 -0
- package/src/content-schema.ts +166 -0
- package/src/content.ts +422 -0
- package/src/document.ts +579 -0
- package/src/editor/README.md +405 -0
- package/src/editor/addresses.ts +61 -0
- package/src/editor/brand.ts +118 -0
- package/src/editor/client/block-list/BlockList.tsx +342 -0
- package/src/editor/client/block-list/Picker.tsx +105 -0
- package/src/editor/client/block-list/UnknownBlock.tsx +119 -0
- package/src/editor/client/block-list/insert-points.ts +56 -0
- package/src/editor/client/block-list/link.ts +71 -0
- package/src/editor/client/block-list/reorder.ts +105 -0
- package/src/editor/client/block-list/summary.ts +118 -0
- package/src/editor/client/collections/CollectionGrid.tsx +143 -0
- package/src/editor/client/collections/CollectionTable.tsx +406 -0
- package/src/editor/client/collections/table.ts +270 -0
- package/src/editor/client/context.tsx +27 -0
- package/src/editor/client/documents/DocMenu.tsx +154 -0
- package/src/editor/client/documents/actions.ts +192 -0
- package/src/editor/client/documents/dialogs.tsx +687 -0
- package/src/editor/client/editor.ts +995 -0
- package/src/editor/client/fields/ButtonsField.tsx +87 -0
- package/src/editor/client/fields/DateField.tsx +200 -0
- package/src/editor/client/fields/FieldControl.tsx +61 -0
- package/src/editor/client/fields/FieldFrame.tsx +109 -0
- package/src/editor/client/fields/GroupField.tsx +113 -0
- package/src/editor/client/fields/HiddenToggle.tsx +51 -0
- package/src/editor/client/fields/LinkField.tsx +307 -0
- package/src/editor/client/fields/ListField.tsx +642 -0
- package/src/editor/client/fields/LockupField.tsx +365 -0
- package/src/editor/client/fields/MediaField.tsx +823 -0
- package/src/editor/client/fields/PageFields.tsx +82 -0
- package/src/editor/client/fields/ReferenceField.tsx +209 -0
- package/src/editor/client/fields/RichTextField.tsx +306 -0
- package/src/editor/client/fields/SelectField.tsx +63 -0
- package/src/editor/client/fields/SeoGroup.tsx +282 -0
- package/src/editor/client/fields/SimpleFields.tsx +89 -0
- package/src/editor/client/fields/TextField.tsx +51 -0
- package/src/editor/client/fields/date.ts +73 -0
- package/src/editor/client/fields/host.tsx +172 -0
- package/src/editor/client/fields/list-summary.ts +85 -0
- package/src/editor/client/fields/lockup-rules.ts +181 -0
- package/src/editor/client/fields/parts.tsx +458 -0
- package/src/editor/client/fields/rich-text-commands.ts +183 -0
- package/src/editor/client/fields/rich-text-doc.ts +156 -0
- package/src/editor/client/fields/rich-text-schema.ts +224 -0
- package/src/editor/client/fields/stale.ts +58 -0
- package/src/editor/client/inbox/InboxStage.tsx +272 -0
- package/src/editor/client/inbox/store.ts +311 -0
- package/src/editor/client/live/format.ts +77 -0
- package/src/editor/client/live/marks.tsx +90 -0
- package/src/editor/client/live/notices.tsx +213 -0
- package/src/editor/client/live/publish.tsx +617 -0
- package/src/editor/client/live/session.tsx +162 -0
- package/src/editor/client/main.tsx +12 -0
- package/src/editor/client/media/LibraryStage.tsx +75 -0
- package/src/editor/client/media/ReplaceDialog.tsx +355 -0
- package/src/editor/client/media/store.ts +618 -0
- package/src/editor/client/nav/DocRow.tsx +122 -0
- package/src/editor/client/nav/search.ts +71 -0
- package/src/editor/client/nav/tree.ts +93 -0
- package/src/editor/client/overlays/OverlayMount.tsx +26 -0
- package/src/editor/client/overlays/editor-marks.ts +159 -0
- package/src/editor/client/overlays/index.ts +50 -0
- package/src/editor/client/overlays/layer.ts +437 -0
- package/src/editor/client/overlays/overlay-css.ts +177 -0
- package/src/editor/client/overlays/rich-text-bar.ts +717 -0
- package/src/editor/client/pane/Pane.tsx +91 -0
- package/src/editor/client/pane/levels/BlockLevel.tsx +123 -0
- package/src/editor/client/pane/levels/CollectionLevel.tsx +115 -0
- package/src/editor/client/pane/levels/DocumentLevel.tsx +93 -0
- package/src/editor/client/pane/levels/DocumentsLevel.tsx +390 -0
- package/src/editor/client/pane/levels/FileLevel.tsx +132 -0
- package/src/editor/client/pane/levels/FormLevel.tsx +550 -0
- package/src/editor/client/pane/levels/MediaLevel.tsx +657 -0
- package/src/editor/client/pane/levels/SettingsM6.tsx +299 -0
- package/src/editor/client/pane/levels/SubmissionsLevel.tsx +185 -0
- package/src/editor/client/pane/parts.tsx +179 -0
- package/src/editor/client/pane/switcher.tsx +165 -0
- package/src/editor/client/preview/driver.ts +640 -0
- package/src/editor/client/preview/field-text.ts +71 -0
- package/src/editor/client/preview/follow-links.ts +234 -0
- package/src/editor/client/preview/layout.ts +37 -0
- package/src/editor/client/preview/link-target.ts +50 -0
- package/src/editor/client/preview/markers.ts +86 -0
- package/src/editor/client/preview/page-edit.ts +632 -0
- package/src/editor/client/preview/page-editor-registry.ts +63 -0
- package/src/editor/client/preview/page-editor.ts +235 -0
- package/src/editor/client/preview/render-sequencer.ts +113 -0
- package/src/editor/client/preview/selection.ts +116 -0
- package/src/editor/client/preview/warnings.ts +61 -0
- package/src/editor/client/shell/App.tsx +101 -0
- package/src/editor/client/shell/Bar.tsx +197 -0
- package/src/editor/client/shell/Brand.tsx +33 -0
- package/src/editor/client/shell/Icons.tsx +87 -0
- package/src/editor/client/shell/NarrowNote.tsx +47 -0
- package/src/editor/client/shell/Popovers.tsx +47 -0
- package/src/editor/client/shell/Stage.tsx +132 -0
- package/src/editor/client/shell/Toast.tsx +61 -0
- package/src/editor/client/state/document-store.ts +465 -0
- package/src/editor/client/state/keeper.ts +713 -0
- package/src/editor/client/state/live.ts +257 -0
- package/src/editor/client/state/schema.ts +42 -0
- package/src/editor/client/state/store.ts +39 -0
- package/src/editor/fonts/OFL-GeistMono.txt +93 -0
- package/src/editor/fonts/OFL-InstrumentSans.txt +93 -0
- package/src/editor/fonts/geist-mono-latin-ext-wght-normal.woff2 +0 -0
- package/src/editor/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
- package/src/editor/fonts/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
- package/src/editor/fonts/instrument-sans-latin-wght-normal.woff2 +0 -0
- package/src/editor/middleware.ts +62 -0
- package/src/editor/routes/_live.ts +26 -0
- package/src/editor/routes/admin.astro +80 -0
- package/src/editor/routes/create.ts +19 -0
- package/src/editor/routes/delete.ts +19 -0
- package/src/editor/routes/document.ts +39 -0
- package/src/editor/routes/documents.ts +30 -0
- package/src/editor/routes/github.ts +23 -0
- package/src/editor/routes/history.ts +9 -0
- package/src/editor/routes/lock.ts +9 -0
- package/src/editor/routes/login/assets.d.ts +10 -0
- package/src/editor/routes/login/login.ts +43 -0
- package/src/editor/routes/media.ts +16 -0
- package/src/editor/routes/move.ts +17 -0
- package/src/editor/routes/notice.ts +9 -0
- package/src/editor/routes/preview.astro +77 -0
- package/src/editor/routes/publish.ts +9 -0
- package/src/editor/routes/status.ts +9 -0
- package/src/editor/routes/submissions.ts +16 -0
- package/src/editor/routes/try-again.ts +9 -0
- package/src/editor/routes/write.ts +18 -0
- package/src/editor/server/admin-props.ts +151 -0
- package/src/editor/server/cards.ts +61 -0
- package/src/editor/server/create.ts +414 -0
- package/src/editor/server/drafts-store.ts +127 -0
- package/src/editor/server/media/alt.ts +107 -0
- package/src/editor/server/media/bucket.ts +295 -0
- package/src/editor/server/media/config.ts +78 -0
- package/src/editor/server/media/handler.ts +221 -0
- package/src/editor/server/media/library.ts +254 -0
- package/src/editor/server/media/memory.ts +184 -0
- package/src/editor/server/media/mux.ts +241 -0
- package/src/editor/server/media/sniff.ts +599 -0
- package/src/editor/server/media/upload.ts +175 -0
- package/src/editor/server/mode.ts +63 -0
- package/src/editor/server/move.ts +295 -0
- package/src/editor/server/render.tsx +9 -0
- package/src/editor/server/replace.ts +72 -0
- package/src/editor/server/settings.ts +35 -0
- package/src/editor/server/write.ts +366 -0
- package/src/editor/styles/editor.css +502 -0
- package/src/editor/styles/fonts.css +40 -0
- package/src/editor/styles/levels/README.md +4 -0
- package/src/editor/styles/levels/collections.css +99 -0
- package/src/editor/styles/levels/document-level.css +10 -0
- package/src/editor/styles/levels/documents.css +9 -0
- package/src/editor/styles/levels/fields.css +171 -0
- package/src/editor/styles/levels/files.css +95 -0
- package/src/editor/styles/levels/live.css +10 -0
- package/src/editor/styles/levels/m5a-documents.css +51 -0
- package/src/editor/styles/levels/m6.css +82 -0
- package/src/editor/styles/levels/media.css +175 -0
- package/src/editor/styles/levels/switcher.css +3 -0
- package/src/editor/styles/tokens.css +114 -0
- package/src/editor/tsconfig.json +16 -0
- package/src/editor/types.ts +881 -0
- package/src/editor/ui-state.ts +113 -0
- package/src/email.ts +67 -0
- package/src/fields.ts +1129 -0
- package/src/files-route.ts +201 -0
- package/src/files.ts +116 -0
- package/src/forms/email.ts +149 -0
- package/src/forms/endpoint.ts +347 -0
- package/src/forms/inbox.ts +273 -0
- package/src/forms/rate-limit.ts +130 -0
- package/src/forms/route.ts +24 -0
- package/src/forms/rules.ts +278 -0
- package/src/forms/store.ts +486 -0
- package/src/head.ts +125 -0
- package/src/index.ts +434 -0
- package/src/json-source.ts +116 -0
- package/src/links.ts +121 -0
- package/src/live/README.md +43 -0
- package/src/live/auth/allow-rules.ts +70 -0
- package/src/live/auth/allow.ts +128 -0
- package/src/live/auth/auth.ts +155 -0
- package/src/live/auth/email.ts +84 -0
- package/src/live/auth/guard.ts +111 -0
- package/src/live/auth/index.ts +68 -0
- package/src/live/auth/links.ts +101 -0
- package/src/live/auth/login.ts +245 -0
- package/src/live/auth/middleware.ts +53 -0
- package/src/live/auth/request.ts +56 -0
- package/src/live/auth/routes.ts +9 -0
- package/src/live/auth/session.ts +181 -0
- package/src/live/auth/views.ts +355 -0
- package/src/live/autosaves.ts +306 -0
- package/src/live/backend.ts +652 -0
- package/src/live/branches.ts +19 -0
- package/src/live/drafts-store.ts +591 -0
- package/src/live/editor.ts +465 -0
- package/src/live/git.ts +1021 -0
- package/src/live/github.ts +271 -0
- package/src/live/locals.ts +24 -0
- package/src/live/locks.ts +273 -0
- package/src/live/merge.ts +197 -0
- package/src/live/messages.ts +151 -0
- package/src/live/mode.ts +210 -0
- package/src/live/redis.ts +533 -0
- package/src/live/runtime.ts +164 -0
- package/src/live/status.ts +109 -0
- package/src/live/sweep.ts +228 -0
- package/src/live/timings.ts +47 -0
- package/src/live/webhook.ts +39 -0
- package/src/lockup-css.ts +58 -0
- package/src/measure.ts +145 -0
- package/src/media-value.ts +239 -0
- package/src/media.ts +255 -0
- package/src/read.ts +256 -0
- package/src/redirect-middleware.ts +49 -0
- package/src/redirects.ts +76 -0
- package/src/relink.ts +293 -0
- package/src/reserved.ts +24 -0
- package/src/rich-text.ts +690 -0
- package/src/schema-key.ts +63 -0
- package/src/seo.ts +124 -0
- package/src/serialize.ts +697 -0
- package/src/skipped.ts +35 -0
- package/src/slug.ts +121 -0
- package/src/text-styles.ts +62 -0
- package/src/virtual.d.ts +28 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live mode's connection to the server (M4 Decisions 9, 11 and 13): the status poll
|
|
3
|
+
* and the calls the bar and the pane make (Publish, Try again, history, a notice's
|
|
4
|
+
* Dismiss, Edit here, Sign out). Local mode never makes one (M3's editor).
|
|
5
|
+
*
|
|
6
|
+
* - **The poll**: `GET /_caracms/status?file=<the open document>` every `POLL_MS`
|
|
7
|
+
* while the tab is visible, and at once when it becomes visible again. It carries
|
|
8
|
+
* the tab's id, so it's also the lock's heartbeat and the tab's presence.
|
|
9
|
+
* - **Last-known data**: a poll that can't reach the server (a deploy replacing it,
|
|
10
|
+
* which Publish causes) keeps the last status and says `reachable: false`. Nothing
|
|
11
|
+
* is lost and nothing shows as an error; the next answer brings it back.
|
|
12
|
+
* - **Signed out**: any 401 (`SignedOutError`) sets `signedOut`; the next poll that
|
|
13
|
+
* answers 200 clears it (signing in from another tab brings the session back).
|
|
14
|
+
*/
|
|
15
|
+
import { ENDPOINTS, TAB_HEADER, type HistoryResponse, type LiveProps, type LockRefusal, type LockResponse, type PublishResponse, type SignedOutError, type StatusResponse, type TryAgainResponse } from '../../types.ts';
|
|
16
|
+
import { createStore, type Store } from './store.ts';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Decision 13: every 5 s while the tab is visible. (The spec's Open questions may
|
|
20
|
+
* shorten it to 3 s while someone else is on the same document; not done unless
|
|
21
|
+
* Rich says so.)
|
|
22
|
+
*/
|
|
23
|
+
export const POLL_MS = 5000;
|
|
24
|
+
|
|
25
|
+
/** Where sign out goes (Phase 3's route): `{ ok, location }` to a JSON request. */
|
|
26
|
+
export const SIGN_OUT = '/admin/login/signout';
|
|
27
|
+
|
|
28
|
+
export interface LiveState {
|
|
29
|
+
/** The last status the server gave (the page's own, from the server, until the first poll). */
|
|
30
|
+
status: StatusResponse | null;
|
|
31
|
+
/** The last poll got an answer. False while the server can't be reached (a deploy, say). */
|
|
32
|
+
reachable: boolean;
|
|
33
|
+
/** The session ended (a 401), until a poll answers 200 again. */
|
|
34
|
+
signedOut: SignedOutError | null;
|
|
35
|
+
/** This tab's own poll has answered: the page's first status had no tab, so its locks of yours can't say which tab. */
|
|
36
|
+
polled: boolean;
|
|
37
|
+
/** Which poll this answer came from (they're numbered as they start). */
|
|
38
|
+
seq: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface LiveSession {
|
|
42
|
+
readonly tab: string;
|
|
43
|
+
readonly state: Store<LiveState>;
|
|
44
|
+
/** How many polls have started: a lock answer newer than this outranks the poll in flight. */
|
|
45
|
+
started(): number;
|
|
46
|
+
poll(): Promise<void>;
|
|
47
|
+
/** Browser only: the poll loop and the visibility listener. Returns a stop. */
|
|
48
|
+
start(): () => void;
|
|
49
|
+
/** The server's answer, or why it didn't publish (a non-200, or the server out of reach): a failure is never shown as published. */
|
|
50
|
+
publish(): Promise<{ ok: true; body: PublishResponse } | { ok: false; error: string }>;
|
|
51
|
+
tryAgain(): Promise<TryAgainResponse>;
|
|
52
|
+
history(): Promise<HistoryResponse | null>;
|
|
53
|
+
dismiss(id: string): Promise<void>;
|
|
54
|
+
/** Edit here: take the open document's lock (moves your own at once). */
|
|
55
|
+
take(file: string): Promise<{ ok: true; lock: LockResponse } | { ok: false; refusal: LockRefusal | null }>;
|
|
56
|
+
/** Take over: move anyone's lock on the document to this tab (their autosave is committed first). */
|
|
57
|
+
takeOver(file: string): Promise<{ ok: true; lock: LockResponse } | { ok: false }>;
|
|
58
|
+
/** Someone typed, clicked or scrolled in this tab: each poll says how long ago (the lock's idle rule). */
|
|
59
|
+
active(): void;
|
|
60
|
+
signOut(): Promise<string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface LiveOptions {
|
|
64
|
+
fetch: typeof fetch;
|
|
65
|
+
props: LiveProps;
|
|
66
|
+
tab: string;
|
|
67
|
+
/** The open document's file, named by every poll. */
|
|
68
|
+
openFile: () => string | null;
|
|
69
|
+
now?: () => number;
|
|
70
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
71
|
+
clearTimer?: (timer: unknown) => void;
|
|
72
|
+
log?: (message: string) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The tab's id: kept for the tab's life in sessionStorage, so a reload is the same tab (its lock stays its own). */
|
|
76
|
+
export function tabId(): string {
|
|
77
|
+
const make = () => (typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `t${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`);
|
|
78
|
+
try {
|
|
79
|
+
const s = window.sessionStorage;
|
|
80
|
+
let id = s.getItem('caracms:tab');
|
|
81
|
+
if (!id) s.setItem('caracms:tab', (id = make()));
|
|
82
|
+
return id;
|
|
83
|
+
} catch {
|
|
84
|
+
return make();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** What counts as someone using the tab, in the editor and in the previews (the lock's idle rule). */
|
|
89
|
+
export const ACTIVITY_EVENTS = ['keydown', 'pointerdown', 'wheel', 'input'] as const;
|
|
90
|
+
|
|
91
|
+
export function createLiveSession(options: LiveOptions): LiveSession {
|
|
92
|
+
const setTimer = options.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
93
|
+
const clearTimer = options.clearTimer ?? ((t) => clearTimeout(t as ReturnType<typeof setTimeout>));
|
|
94
|
+
const log = options.log ?? ((message) => console.warn(`[caracms] ${message}`));
|
|
95
|
+
const state = createStore<LiveState>({ status: options.props.status, reachable: true, signedOut: null, polled: false, seq: 0 });
|
|
96
|
+
let seq = 0;
|
|
97
|
+
const now = options.now ?? (() => Date.now());
|
|
98
|
+
let lastInput = now();
|
|
99
|
+
let inFlight: Promise<void> | null = null;
|
|
100
|
+
let again = false;
|
|
101
|
+
|
|
102
|
+
const headers = (json: boolean): Record<string, string> => ({ Accept: 'application/json', [TAB_HEADER]: options.tab, ...(json ? { 'Content-Type': 'application/json' } : {}) });
|
|
103
|
+
|
|
104
|
+
/** A call to an editor route: the answer's body, or null when the server couldn't be reached. A 401 marks the session ended. */
|
|
105
|
+
async function call<T>(path: string, init: { method?: string; body?: unknown } = {}): Promise<{ status: number; body: T } | null> {
|
|
106
|
+
try {
|
|
107
|
+
const response = await options.fetch(path, {
|
|
108
|
+
method: init.method ?? 'GET',
|
|
109
|
+
headers: headers(init.body !== undefined),
|
|
110
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
111
|
+
});
|
|
112
|
+
const body = (await response.json().catch(() => ({}))) as T;
|
|
113
|
+
if (response.status === 401) {
|
|
114
|
+
const b = body as unknown as Partial<SignedOutError>;
|
|
115
|
+
state.update({ signedOut: { error: b.error ?? 'signed-out', code: 'signed-out', reason: b.reason === 'removed' ? 'removed' : 'signed-out', signIn: b.signIn ?? '/admin/login' }, reachable: true });
|
|
116
|
+
}
|
|
117
|
+
return { status: response.status, body };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
log(`${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function pollOnce(): Promise<void> {
|
|
125
|
+
const mine = ++seq;
|
|
126
|
+
const file = options.openFile();
|
|
127
|
+
// `input`: ms since someone used this tab. The server stops renewing an idle tab's lock.
|
|
128
|
+
const query = new URLSearchParams({ ...(file ? { file } : {}), input: String(Math.max(0, Math.round(now() - lastInput))) });
|
|
129
|
+
const answer = await call<StatusResponse>(`${ENDPOINTS.status}?${query}`);
|
|
130
|
+
if (!answer) {
|
|
131
|
+
state.update({ reachable: false });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (answer.status === 200 && answer.body && (answer.body as StatusResponse).mode === 'live') {
|
|
135
|
+
state.set((s) => ({ ...s, status: answer.body, reachable: true, signedOut: null, polled: true, seq: mine }));
|
|
136
|
+
} else if (answer.status === 401) {
|
|
137
|
+
// call() marked it.
|
|
138
|
+
} else if (answer.status >= 500 || answer.status === 404) {
|
|
139
|
+
// 503 unavailable (Redis), a deploy's gap, or the route not there yet: the server isn't answering for now.
|
|
140
|
+
state.update({ reachable: false });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function poll(): Promise<void> {
|
|
145
|
+
if (inFlight) {
|
|
146
|
+
again = true;
|
|
147
|
+
return inFlight;
|
|
148
|
+
}
|
|
149
|
+
inFlight = (async () => {
|
|
150
|
+
try {
|
|
151
|
+
do {
|
|
152
|
+
again = false;
|
|
153
|
+
await pollOnce();
|
|
154
|
+
} while (again);
|
|
155
|
+
} finally {
|
|
156
|
+
inFlight = null;
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
return inFlight;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const session: LiveSession = {
|
|
163
|
+
tab: options.tab,
|
|
164
|
+
state,
|
|
165
|
+
started: () => seq,
|
|
166
|
+
poll,
|
|
167
|
+
start() {
|
|
168
|
+
let timer: unknown = null;
|
|
169
|
+
let stopped = false;
|
|
170
|
+
/** One loop at a time: a restart starts a new generation, and the old one stops at its next turn. */
|
|
171
|
+
let generation = 0;
|
|
172
|
+
const visible = () => typeof document === 'undefined' || document.visibilityState !== 'hidden';
|
|
173
|
+
const loop = (g: number) => {
|
|
174
|
+
if (stopped || g !== generation) return;
|
|
175
|
+
timer = setTimer(async () => {
|
|
176
|
+
timer = null;
|
|
177
|
+
if (g !== generation) return;
|
|
178
|
+
if (visible()) await poll();
|
|
179
|
+
loop(g);
|
|
180
|
+
}, options.props.pollMs ?? POLL_MS);
|
|
181
|
+
};
|
|
182
|
+
const restart = () => {
|
|
183
|
+
const g = ++generation;
|
|
184
|
+
if (timer !== null) clearTimer(timer);
|
|
185
|
+
timer = null;
|
|
186
|
+
void poll().finally(() => loop(g));
|
|
187
|
+
};
|
|
188
|
+
// Visible again: at once (Decision 13). The lock is re-taken by that poll if nobody else has it (Decision 9).
|
|
189
|
+
const onVisible = () => {
|
|
190
|
+
if (!visible()) return;
|
|
191
|
+
lastInput = now(); // coming back to the tab counts as using it
|
|
192
|
+
restart();
|
|
193
|
+
};
|
|
194
|
+
const onActivity = () => {
|
|
195
|
+
lastInput = now();
|
|
196
|
+
};
|
|
197
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
198
|
+
for (const type of ACTIVITY_EVENTS) document.addEventListener(type, onActivity, { capture: true, passive: true });
|
|
199
|
+
restart();
|
|
200
|
+
return () => {
|
|
201
|
+
stopped = true;
|
|
202
|
+
if (timer !== null) clearTimer(timer);
|
|
203
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
204
|
+
for (const type of ACTIVITY_EVENTS) document.removeEventListener(type, onActivity, { capture: true });
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
async publish() {
|
|
208
|
+
const answer = await call<PublishResponse>(ENDPOINTS.publish, { method: 'POST', body: {} });
|
|
209
|
+
void poll();
|
|
210
|
+
if (!answer) return { ok: false, error: "The server couldn't be reached." };
|
|
211
|
+
if (answer.status !== 200 || !answer.body?.publish) {
|
|
212
|
+
const said = (answer.body as unknown as { error?: unknown } | null)?.error;
|
|
213
|
+
return { ok: false, error: typeof said === 'string' && said ? said : `The server answered ${answer.status}.` };
|
|
214
|
+
}
|
|
215
|
+
state.set((s) => (s.status ? { ...s, status: { ...s.status, publish: answer.body.publish } } : s));
|
|
216
|
+
return { ok: true, body: answer.body };
|
|
217
|
+
},
|
|
218
|
+
async tryAgain() {
|
|
219
|
+
const answer = await call<TryAgainResponse>(ENDPOINTS.tryAgain, { method: 'POST', body: {} });
|
|
220
|
+
void poll();
|
|
221
|
+
if (!answer) return { ok: false, error: "Couldn't reach the server." };
|
|
222
|
+
return answer.body;
|
|
223
|
+
},
|
|
224
|
+
async history() {
|
|
225
|
+
const answer = await call<HistoryResponse>(ENDPOINTS.history);
|
|
226
|
+
return answer && answer.status === 200 ? answer.body : null;
|
|
227
|
+
},
|
|
228
|
+
async dismiss(id) {
|
|
229
|
+
// Gone at once; the server agrees on the next poll.
|
|
230
|
+
state.set((s) => (s.status ? { ...s, status: { ...s.status, notices: s.status.notices.filter((n) => n.id !== id) } } : s));
|
|
231
|
+
await call(ENDPOINTS.notice, { method: 'POST', body: { id } });
|
|
232
|
+
},
|
|
233
|
+
active() {
|
|
234
|
+
lastInput = now();
|
|
235
|
+
},
|
|
236
|
+
async takeOver(file) {
|
|
237
|
+
const answer = await call<LockResponse>(ENDPOINTS.lock, { method: 'POST', body: { file, action: 'take-over' } });
|
|
238
|
+
void poll();
|
|
239
|
+
return answer?.status === 200 ? { ok: true, lock: answer.body } : { ok: false };
|
|
240
|
+
},
|
|
241
|
+
async take(file) {
|
|
242
|
+
const answer = await call<LockResponse | LockRefusal>(ENDPOINTS.lock, { method: 'POST', body: { file, action: 'take' } });
|
|
243
|
+
if (answer?.status === 200) return { ok: true, lock: answer.body as LockResponse };
|
|
244
|
+
return { ok: false, refusal: answer?.status === 409 ? (answer.body as LockRefusal) : null };
|
|
245
|
+
},
|
|
246
|
+
async signOut() {
|
|
247
|
+
try {
|
|
248
|
+
const response = await options.fetch(SIGN_OUT, { method: 'POST', headers: { Accept: 'application/json' } });
|
|
249
|
+
const body = (await response.json().catch(() => ({}))) as { location?: string };
|
|
250
|
+
return body.location ?? '/admin/login';
|
|
251
|
+
} catch {
|
|
252
|
+
return '/admin/login';
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
return session;
|
|
257
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fields a block of the open document has (M5a Phase 5): a block's schema in a
|
|
3
|
+
* section builder or a shared block, or, for the one block a fields document or Site
|
|
4
|
+
* Settings is held as (document-store.ts), its collection's fields. Pure, so the
|
|
5
|
+
* overlays, the page's typing and the driver read the same thing.
|
|
6
|
+
*/
|
|
7
|
+
import { EDITORS_FIELD, own } from '../../../document.ts';
|
|
8
|
+
import type { Fields } from '../../../fields.ts';
|
|
9
|
+
import type { EditorDocument, EditorSite } from '../../types.ts';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/** The fields of a fields document's collection, or of Site Settings (the site's own `settings.fields`). */
|
|
13
|
+
export function formFields(site: Pick<EditorSite, 'collections'>, collection: string): Fields {
|
|
14
|
+
return site.collections.find((c) => c.name === collection)?.fields ?? {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The block types `collection`'s pages offer (config.ts `blocks`): null for every block. */
|
|
18
|
+
export function offeredBlocks(site: Pick<EditorSite, 'collections'>, collection: string): readonly string[] | null {
|
|
19
|
+
return site.collections.find((c) => c.name === collection)?.blocks ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Whether `collection`'s pages offer blocks of `type` (always, when it names none). */
|
|
23
|
+
export function offersBlock(site: Pick<EditorSite, 'collections'>, collection: string, type: string): boolean {
|
|
24
|
+
const offered = offeredBlocks(site, collection);
|
|
25
|
+
return !offered || offered.includes(type);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The fields the block `blockType` has in a document of `kind` from `collection`: a
|
|
30
|
+
* form's (fields, settings) are its collection's; a block's are its type's schema.
|
|
31
|
+
*/
|
|
32
|
+
export function fieldsOfBlock(
|
|
33
|
+
site: Pick<EditorSite, 'blocks' | 'collections'>,
|
|
34
|
+
document: Pick<EditorDocument, 'kind'>,
|
|
35
|
+
collection: string,
|
|
36
|
+
block: { type: string; known: boolean },
|
|
37
|
+
): Fields {
|
|
38
|
+
if (document.kind === 'fields') return formFields(site, collection);
|
|
39
|
+
if (document.kind === 'settings') return { editors: EDITORS_FIELD, ...formFields(site, collection) };
|
|
40
|
+
if (!block.known) return {};
|
|
41
|
+
return own(site.blocks, block.type)?.fields ?? {};
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny observable value: the editor's state containers are built on it, and
|
|
3
|
+
* React reads them with `useStore` (context.tsx → useSyncExternalStore). Values are
|
|
4
|
+
* replaced, never mutated, so a snapshot is stable until the next `set`.
|
|
5
|
+
*/
|
|
6
|
+
export interface Store<T> {
|
|
7
|
+
get(): T;
|
|
8
|
+
set(next: T | ((previous: T) => T)): void;
|
|
9
|
+
/** Merge into an object value. */
|
|
10
|
+
update(patch: Partial<T>): void;
|
|
11
|
+
subscribe(listener: (value: T, previous: T) => void): () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createStore<T>(initial: T): Store<T> {
|
|
15
|
+
let value = initial;
|
|
16
|
+
const listeners = new Set<(value: T, previous: T) => void>();
|
|
17
|
+
const store: Store<T> = {
|
|
18
|
+
get: () => value,
|
|
19
|
+
set(next) {
|
|
20
|
+
const previous = value;
|
|
21
|
+
value = typeof next === 'function' ? (next as (p: T) => T)(previous) : next;
|
|
22
|
+
if (Object.is(value, previous)) return;
|
|
23
|
+
for (const listener of [...listeners]) listener(value, previous);
|
|
24
|
+
},
|
|
25
|
+
update(patch) {
|
|
26
|
+
store.set((previous) => {
|
|
27
|
+
for (const key of Object.keys(patch) as (keyof T)[]) {
|
|
28
|
+
if (!Object.is(previous[key], patch[key])) return { ...previous, ...patch };
|
|
29
|
+
}
|
|
30
|
+
return previous;
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
subscribe(listener) {
|
|
34
|
+
listeners.add(listener);
|
|
35
|
+
return () => listeners.delete(listener);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
return store;
|
|
39
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git) GeistMono-Italic[wght].ttf: Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) InstrumentSans-Italic[wdth,wght].ttf: Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unconfigured server's answer (M4 Decision 1): a production server missing a
|
|
3
|
+
* required variable still serves the public site, while `/admin` and `/_caracms/*`
|
|
4
|
+
* answer 503 with a plain sentence naming what's missing (logged once, server/mode.ts).
|
|
5
|
+
*
|
|
6
|
+
* Registered first (`addMiddleware({ order: 'pre' })` in ../index.ts), so it runs
|
|
7
|
+
* before Phase 3's sign-in guard, which then only ever sees local or live mode.
|
|
8
|
+
* Every other path passes straight through, and so does the public form endpoint,
|
|
9
|
+
* `/_caracms/form` (M6 Decision 5): the form is part of the public site, which works
|
|
10
|
+
* without the editor.
|
|
11
|
+
*
|
|
12
|
+
* It also marks the preview route's pages (M8 review): a 200 HTML answer to the preview's GET
|
|
13
|
+
* gets `data-caracms-preview` on its `<html>` (types.ts → PREVIEW_MARK), the one sign the
|
|
14
|
+
* driver has that a frame loaded a preview and not an error page. The site's page component
|
|
15
|
+
* writes the `<html>`, so the route itself can't. Everything else, a POST's render included
|
|
16
|
+
* (only its body is morphed), passes untouched.
|
|
17
|
+
*/
|
|
18
|
+
import type { MiddlewareHandler } from 'astro';
|
|
19
|
+
import { isFormEndpoint } from '../live/auth/guard.ts';
|
|
20
|
+
import { unconfiguredMessage } from '../live/mode.ts';
|
|
21
|
+
import { editorMode } from './server/mode.ts';
|
|
22
|
+
import { ENDPOINTS, PREVIEW_MARK } from './types.ts';
|
|
23
|
+
|
|
24
|
+
/** `html` with the preview's mark on its `<html>` (one is added after the doctype when the page has none). */
|
|
25
|
+
export function markPreviewHtml(html: string): string {
|
|
26
|
+
if (/<html\b/i.test(html)) return html.replace(/<html\b/i, `<html ${PREVIEW_MARK}`);
|
|
27
|
+
const doctype = /^\s*<!doctype[^>]*>/i.exec(html)?.[0] ?? '';
|
|
28
|
+
return `${doctype}<html ${PREVIEW_MARK}>${html.slice(doctype.length)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function markPreview(response: Response): Promise<Response> {
|
|
32
|
+
if (response.status !== 200 || !/^text\/html/i.test(response.headers.get('Content-Type') ?? '')) return response;
|
|
33
|
+
const headers = new Headers(response.headers);
|
|
34
|
+
headers.delete('Content-Length');
|
|
35
|
+
return new Response(markPreviewHtml(await response.text()), { status: response.status, statusText: response.statusText, headers });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const isEditorPath = (pathname: string) =>
|
|
39
|
+
(pathname === '/admin' || pathname.startsWith('/admin/') || pathname.startsWith('/_caracms/')) && !isFormEndpoint(pathname);
|
|
40
|
+
|
|
41
|
+
export const onRequest: MiddlewareHandler = (context, next) => {
|
|
42
|
+
if (!isEditorPath(context.url.pathname)) return next();
|
|
43
|
+
const mode = editorMode();
|
|
44
|
+
if (mode.kind !== 'unconfigured') {
|
|
45
|
+
if (context.url.pathname !== ENDPOINTS.preview || context.request.method !== 'GET') return next();
|
|
46
|
+
return (async () => markPreview(await next()))();
|
|
47
|
+
}
|
|
48
|
+
const message = unconfiguredMessage(mode);
|
|
49
|
+
const api = context.url.pathname.startsWith('/_caracms/');
|
|
50
|
+
const escape = (s: string) => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]!);
|
|
51
|
+
return Promise.resolve(
|
|
52
|
+
api
|
|
53
|
+
? new Response(JSON.stringify({ error: message, code: 'unconfigured' }), {
|
|
54
|
+
status: 503,
|
|
55
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' },
|
|
56
|
+
})
|
|
57
|
+
: new Response(`<!doctype html><meta charset="utf-8"><meta name="robots" content="noindex"><title>Editor not set up</title><p>${escape(message)}</p>`, {
|
|
58
|
+
status: 503,
|
|
59
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' },
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live mode's routes (M4 Phase 2) share this: each is one handler on the process's live
|
|
3
|
+
* editor (../../live/editor.ts), given the signed-in person from `locals`. In local
|
|
4
|
+
* mode they answer 404 `{ code: 'not-live' }`: M3's editor has no status, locks or
|
|
5
|
+
* Publish (Decision 1).
|
|
6
|
+
*/
|
|
7
|
+
import type { APIRoute } from 'astro';
|
|
8
|
+
import { site } from 'virtual:caracms/config';
|
|
9
|
+
import type { LiveEditor } from '../../live/editor.ts';
|
|
10
|
+
import { personOf } from '../../live/locals.ts';
|
|
11
|
+
import { liveEditorOf } from '../server/mode.ts';
|
|
12
|
+
|
|
13
|
+
type Name = 'status' | 'lock' | 'publish' | 'history' | 'tryAgain' | 'notice';
|
|
14
|
+
|
|
15
|
+
export const liveRoute =
|
|
16
|
+
(name: Name): APIRoute =>
|
|
17
|
+
async ({ request, locals }) => {
|
|
18
|
+
const live: LiveEditor | null = await liveEditorOf(site);
|
|
19
|
+
if (!live) {
|
|
20
|
+
return new Response(JSON.stringify({ error: 'This route is for live mode; the editor is in local mode here.', code: 'not-live' }), {
|
|
21
|
+
status: 404,
|
|
22
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return live[name](request, personOf(locals));
|
|
26
|
+
};
|