@north-light/crouter 0.3.198 → 0.3.199
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/dist/api/client.d.ts +18 -18
- package/dist/api/client.js +29 -37
- package/dist/api/dto/broker-ops.d.ts +3 -1
- package/dist/api/dto/broker.d.ts +13 -2
- package/dist/api/dto/broker.js +4 -4
- package/dist/api/dto/human.d.ts +6 -5
- package/dist/api/dto/inbox.d.ts +81 -34
- package/dist/api/dto/nodes.d.ts +8 -2
- package/dist/api/dto/profiles.d.ts +23 -0
- package/dist/api/dto/profiles.js +2 -2
- package/dist/api/dto/worktree.d.ts +3 -5
- package/dist/api/routes.d.ts +3 -5
- package/dist/api/routes.js +4 -6
- package/dist/builtin-memory/00-runtime-base.md +6 -6
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
- package/dist/builtin-memory/insights/capture.md +2 -2
- package/dist/builtin-memory/insights/init.md +3 -3
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
- package/dist/builtin-memory/internal/plugins.md +59 -2
- package/dist/builtin-memory/internal/storage-tiers.md +3 -1
- package/dist/builtin-memory/spec/roadmap.md +2 -2
- package/dist/builtin-memory/testing.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +26 -1
- package/dist/clients/attach/render/chat-view.js +61 -11
- package/dist/clients/attach/render/page-block.d.ts +34 -0
- package/dist/clients/attach/render/page-block.js +226 -0
- package/dist/clients/attach/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -993
- package/dist/clients/conversation/projection.js +18 -3
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +1 -3
- package/dist/clients/inbox/page-adapter.js +3 -4
- package/dist/clients/inbox/resolve.d.ts +1 -1
- package/dist/clients/inbox/resolve.js +3 -3
- package/dist/clients/inbox/review/document-surface.js +3 -1
- package/dist/clients/inbox/review/state.d.ts +2 -1
- package/dist/clients/inbox/review/state.js +5 -16
- package/dist/clients/inbox/tui/input.js +37 -19
- package/dist/clients/inbox/tui/panel.js +5 -31
- package/dist/clients/inbox/tui/render.d.ts +11 -0
- package/dist/clients/inbox/tui/render.js +71 -27
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +16 -12
- package/dist/clients/inbox/tui/types.d.ts +2 -7
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +71 -114
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-snapshot.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
- package/dist/commands/human/components.js +77 -0
- package/dist/commands/human/feedback.d.ts +2 -0
- package/dist/commands/human/feedback.js +108 -0
- package/dist/commands/human/prompts.d.ts +13 -5
- package/dist/commands/human/prompts.js +160 -97
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +2 -12
- package/dist/commands/human/shared.js +17 -11
- package/dist/commands/human.js +29 -21
- package/dist/commands/memory/origin.js +2 -2
- package/dist/commands/node/bash.js +4 -4
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +2 -2
- package/dist/commands/node-worktree.js +8 -8
- package/dist/commands/pkg/browse.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +129 -10
- package/dist/commands/profile/delete.js +41 -15
- package/dist/commands/profile.js +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/focus.js +29 -14
- package/dist/commands/surface-edit.js +3 -3
- package/dist/commands/surface-inbox.d.ts +2 -0
- package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
- package/dist/commands/surface.js +2 -1
- package/dist/commands/sys/doctor.js +60 -3
- package/dist/commands/sys/feedback.js +79 -25
- package/dist/commands/sys/settings.js +1 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
- package/dist/core/__tests__/extension-abort.test.js +5 -2
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
- package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +31 -26
- package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +157 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
- package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
- package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
- package/dist/core/__tests__/serial/worktree.test.js +24 -198
- package/dist/core/__tests__/session-cycles.test.js +22 -0
- package/dist/core/__tests__/stop-guard.test.js +4 -4
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +10 -0
- package/dist/core/canvas/__tests__/attention.test.js +8 -5
- package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
- package/dist/core/canvas/attention.js +8 -6
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/canvas.d.ts +7 -0
- package/dist/core/canvas/canvas.js +37 -0
- package/dist/core/canvas/crons.d.ts +9 -0
- package/dist/core/canvas/crons.js +51 -0
- package/dist/core/canvas/node-order.d.ts +22 -1
- package/dist/core/canvas/node-order.js +26 -1
- package/dist/core/canvas/render-source.d.ts +5 -0
- package/dist/core/canvas/render-source.js +1 -0
- package/dist/core/canvas/types.d.ts +9 -3
- package/dist/core/command-plugins/bundle.d.ts +6 -1
- package/dist/core/command-plugins/bundle.js +26 -13
- package/dist/core/command.js +2 -1
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +137 -4
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.js +4 -0
- package/dist/core/human/__tests__/page-tickets.test.js +65 -52
- package/dist/core/human/__tests__/page.test.js +56 -98
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
- package/dist/core/human/answer-text.js +3 -3
- package/dist/core/human/answer.d.ts +6 -4
- package/dist/core/human/answer.js +4 -10
- package/dist/core/human/component-docs.d.ts +11 -7
- package/dist/core/human/component-docs.js +361 -88
- package/dist/core/human/convention.d.ts +17 -3
- package/dist/core/human/convention.js +36 -5
- package/dist/core/human/feedback-companion.d.ts +20 -0
- package/dist/core/human/feedback-companion.js +98 -0
- package/dist/core/human/feedback.d.ts +65 -0
- package/dist/core/human/feedback.js +116 -0
- package/dist/core/human/page-catalog.d.ts +40 -5
- package/dist/core/human/page-catalog.js +102 -30
- package/dist/core/human/page-errors.d.ts +4 -0
- package/dist/core/human/page-errors.js +7 -0
- package/dist/core/human/page-eval.d.ts +20 -0
- package/dist/core/human/page-eval.js +123 -0
- package/dist/core/human/page-schema.d.ts +23 -191
- package/dist/core/human/page-schema.js +60 -89
- package/dist/core/human/page-synth.d.ts +1 -1
- package/dist/core/human/page-synth.js +15 -18
- package/dist/core/human/page.d.ts +12 -27
- package/dist/core/human/page.js +142 -474
- package/dist/core/human/root.d.ts +6 -2
- package/dist/core/human/root.js +12 -4
- package/dist/core/human/scan.d.ts +3 -4
- package/dist/core/human/scan.js +16 -15
- package/dist/core/human/summary.d.ts +1 -2
- package/dist/core/human/summary.js +2 -2
- package/dist/core/human/tickets.d.ts +20 -14
- package/dist/core/human/tickets.js +91 -31
- package/dist/core/human/types.d.ts +10 -2
- package/dist/core/preview-registry.js +8 -10
- package/dist/core/profiles/default-binding.d.ts +4 -0
- package/dist/core/profiles/default-binding.js +24 -0
- package/dist/core/profiles/deletion-reservation.d.ts +8 -0
- package/dist/core/profiles/deletion-reservation.js +38 -0
- package/dist/core/profiles/manifest.d.ts +3 -0
- package/dist/core/profiles/manifest.js +12 -0
- package/dist/core/profiles/select.js +2 -2
- package/dist/core/review/realize.js +3 -3
- package/dist/core/review/store.d.ts +6 -0
- package/dist/core/review/store.js +31 -0
- package/dist/core/runtime/bearings.d.ts +0 -4
- package/dist/core/runtime/bearings.js +4 -62
- package/dist/core/runtime/bin-contributions.d.ts +44 -0
- package/dist/core/runtime/bin-contributions.js +186 -0
- package/dist/core/runtime/broker/client-registry.d.ts +4 -0
- package/dist/core/runtime/broker/client-registry.js +13 -1
- package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
- package/dist/core/runtime/broker/extension-abort.js +4 -2
- package/dist/core/runtime/broker/fault-retry.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
- package/dist/core/runtime/broker/frame-dispatch.js +5 -6
- package/dist/core/runtime/broker/read-ops.d.ts +1 -1
- package/dist/core/runtime/broker/read-ops.js +2 -2
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +6 -1
- package/dist/core/runtime/broker-extension-render.js +19 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +17 -6
- package/dist/core/runtime/front-door-env.d.ts +5 -0
- package/dist/core/runtime/front-door-env.js +16 -0
- package/dist/core/runtime/front-door.d.ts +1 -5
- package/dist/core/runtime/front-door.js +2 -5
- package/dist/core/runtime/host.d.ts +1 -1
- package/dist/core/runtime/invocation.d.ts +6 -0
- package/dist/core/runtime/invocation.js +10 -0
- package/dist/core/runtime/kickoff.js +34 -0
- package/dist/core/runtime/launch.d.ts +2 -6
- package/dist/core/runtime/node-read.js +4 -1
- package/dist/core/runtime/nodes.js +2 -0
- package/dist/core/runtime/promote.d.ts +1 -0
- package/dist/core/runtime/promote.js +3 -1
- package/dist/core/runtime/session-cycles.d.ts +6 -0
- package/dist/core/runtime/session-cycles.js +15 -1
- package/dist/core/runtime/session-visibility.d.ts +17 -8
- package/dist/core/runtime/session-visibility.js +19 -10
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +62 -2
- package/dist/core/runtime/stop-guard.d.ts +2 -2
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/stop-signals.d.ts +1 -1
- package/dist/core/runtime/stop-signals.js +2 -2
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/session-model/session-state.d.ts +4 -3
- package/dist/core/session-model/session-state.js +14 -1
- package/dist/core/user-settings.d.ts +21 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/core/worktree.d.ts +15 -45
- package/dist/core/worktree.js +59 -261
- package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +9 -5
- package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/feedback-comments.js +207 -0
- package/dist/daemon/api/handlers/human.js +24 -20
- package/dist/daemon/api/handlers/inbox.d.ts +10 -0
- package/dist/daemon/api/handlers/inbox.js +39 -115
- package/dist/daemon/api/handlers/profiles.js +11 -29
- package/dist/daemon/api/map.d.ts +6 -1
- package/dist/daemon/api/map.js +23 -0
- package/dist/daemon/api/server.js +3 -1
- package/dist/daemon/companion-retire.d.ts +2 -0
- package/dist/daemon/companion-retire.js +33 -0
- package/dist/daemon/cron-run.js +14 -16
- package/dist/daemon/human/finish.d.ts +12 -7
- package/dist/daemon/human/finish.js +101 -38
- package/dist/daemon/human/sweep.js +5 -1
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +3 -1
- package/dist/daemon/review/deliver.js +3 -3
- package/dist/daemon/review/finish.d.ts +0 -2
- package/dist/daemon/review/finish.js +1 -29
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.js +19 -5
- package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
- package/dist/pi-extensions/canvas-review-boundary.js +18 -9
- package/dist/pi-extensions/canvas-stophook.js +4 -4
- package/dist/shared/generated-context.js +2 -2
- package/dist/types.d.ts +44 -2
- package/dist/types.js +7 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/dist/builtin-memory/init.md +0 -38
- package/dist/builtin-memory/plan.md +0 -19
- package/dist/builtin-memory/spec.md +0 -19
- package/dist/commands/human/doc.d.ts +0 -2
- package/dist/commands/human/doc.js +0 -91
- package/dist/commands/human/html.js +0 -81
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/html-markdown.test.js +0 -52
- package/dist/core/human/__tests__/page-render.test.js +0 -66
- package/dist/core/human/html-markdown.d.ts +0 -3
- package/dist/core/human/html-markdown.js +0 -288
- package/dist/core/human/page-render.d.ts +0 -4
- package/dist/core/human/page-render.js +0 -105
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1778
- package/dist/pages/comments.d.ts +0 -72
- package/dist/pages/comments.js +0 -176
- package/dist/pages/controls.d.ts +0 -98
- package/dist/pages/controls.js +0 -261
- package/dist/pages/elements/cards.d.ts +0 -28
- package/dist/pages/elements/cards.js +0 -915
- package/dist/pages/elements/chart.d.ts +0 -23
- package/dist/pages/elements/chart.js +0 -664
- package/dist/pages/elements/options.d.ts +0 -30
- package/dist/pages/elements/options.js +0 -742
- package/dist/pages/elements/pages.d.ts +0 -30
- package/dist/pages/elements/pages.js +0 -377
- package/dist/pages/elements/slot.d.ts +0 -26
- package/dist/pages/elements/slot.js +0 -112
- package/dist/pages/elements/table.d.ts +0 -36
- package/dist/pages/elements/table.js +0 -1043
- package/dist/pages/elements/text.d.ts +0 -31
- package/dist/pages/elements/text.js +0 -1175
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -134
- package/dist/pages/host.js +0 -165
- package/dist/pages/readonly.d.ts +0 -24
- package/dist/pages/readonly.js +0 -31
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/responses.d.ts +0 -37
- package/dist/pages/responses.js +0 -56
- package/dist/pages/slot-config.d.ts +0 -36
- package/dist/pages/slot-config.js +0 -50
- package/dist/pages/types.d.ts +0 -98
- package/dist/pages/types.js +0 -23
- /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
package/dist/core/human/page.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
}
|
|
3
|
+
import { transformSync } from 'esbuild';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { BUILTIN_KIND_BY_TAG, BUILTIN_PAGE_COMPONENT_TAGS, DISPLAY_PAGE_COMPONENT_TAGS, pageComponentTag } from './page-catalog.js';
|
|
6
|
+
import { isResponseBearingSlot, validateAuthoredPageManifest, validatePageManifest } from './page-schema.js';
|
|
7
|
+
import { evaluatePageTree, jsonProps } from './page-eval.js';
|
|
8
|
+
import { PageAuthoringError } from './page-errors.js';
|
|
9
|
+
export { PageAuthoringError };
|
|
12
10
|
function asPageAuthoringError(error) {
|
|
13
11
|
if (error instanceof PageAuthoringError)
|
|
14
12
|
return error;
|
|
@@ -31,443 +29,149 @@ function validateAuthoredManifest(parsed, productComponents) {
|
|
|
31
29
|
throw asPageAuthoringError(error);
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const IMPORTLESS_SCOPE = 'a page is evaluated with React and every registry component already in scope, so it takes no imports — write `<Page>`, `<UserQuestion>`, `<Card>` and friends directly.';
|
|
33
|
+
/** Compile one authored TSX page to the CommonJS module the Gateway evaluates.
|
|
34
|
+
* Registry names stay free identifiers; the Gateway supplies them at eval. */
|
|
35
|
+
export function compilePage(document) {
|
|
36
36
|
try {
|
|
37
|
-
|
|
37
|
+
return transformSync(document, {
|
|
38
|
+
loader: 'tsx',
|
|
39
|
+
format: 'cjs',
|
|
40
|
+
jsx: 'transform',
|
|
41
|
+
jsxFactory: 'React.createElement',
|
|
42
|
+
jsxFragment: 'React.Fragment',
|
|
43
|
+
target: 'es2020',
|
|
44
|
+
sourcefile: 'page.tsx',
|
|
45
|
+
}).code;
|
|
38
46
|
}
|
|
39
47
|
catch (error) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* matter is merely skipped to recover the stored source body.
|
|
72
|
-
*/
|
|
73
|
-
function projectLegacyMarkdownSource(document, productComponents) {
|
|
74
|
-
if (!(document.startsWith('---\n') || document.startsWith('---\r\n'))) {
|
|
75
|
-
throw new PageAuthoringError('stored md page is missing its opening front matter delimiter');
|
|
76
|
-
}
|
|
77
|
-
const firstEnd = document.indexOf('\n') + 1;
|
|
78
|
-
let bodyStart = -1;
|
|
79
|
-
let cursor = firstEnd;
|
|
80
|
-
while (cursor <= document.length) {
|
|
81
|
-
const newline = document.indexOf('\n', cursor);
|
|
82
|
-
const lineEnd = newline === -1 ? document.length : newline;
|
|
83
|
-
if (document.slice(cursor, lineEnd).replace(/\r$/, '') === '---') {
|
|
84
|
-
bodyStart = newline === -1 ? document.length : newline + 1;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
if (newline === -1)
|
|
88
|
-
break;
|
|
89
|
-
cursor = newline + 1;
|
|
90
|
-
}
|
|
91
|
-
if (bodyStart === -1)
|
|
92
|
-
throw new PageAuthoringError('stored md page is missing its closing front matter delimiter');
|
|
93
|
-
const lines = sourceLines(document, bodyStart);
|
|
94
|
-
const slots = [];
|
|
95
|
-
const projection = [];
|
|
96
|
-
let proseStart = bodyStart;
|
|
97
|
-
let ordinaryFence = null;
|
|
98
|
-
for (let index = 0; index < lines.length; index++) {
|
|
99
|
-
const line = lines[index];
|
|
100
|
-
if (ordinaryFence !== null) {
|
|
101
|
-
if (closesFence(line.text, ordinaryFence))
|
|
102
|
-
ordinaryFence = null;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
const opening = line.text.match(/^(`{3,}|~{3,})(.*)$/);
|
|
106
|
-
if (opening === null)
|
|
107
|
-
continue;
|
|
108
|
-
const run = opening[1];
|
|
109
|
-
const fence = { char: run[0], length: run.length };
|
|
110
|
-
const info = opening[2];
|
|
111
|
-
if (fence.char !== '`' || !info.startsWith('slot:')) {
|
|
112
|
-
ordinaryFence = fence;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const kind = info.slice('slot:'.length);
|
|
116
|
-
if (kind === '' || info !== `slot:${kind}` || !isRegisteredPageKind(kind, productComponents)) {
|
|
117
|
-
throw new PageAuthoringError(`unregistered or invalid page slot kind "${kind}" in stored md fence`);
|
|
118
|
-
}
|
|
119
|
-
let closeIndex = -1;
|
|
120
|
-
for (let candidate = index + 1; candidate < lines.length; candidate++) {
|
|
121
|
-
if (closesFence(lines[candidate].text, fence)) {
|
|
122
|
-
closeIndex = candidate;
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (closeIndex === -1)
|
|
127
|
-
throw new PageAuthoringError(`slot:${kind} fence has no matching ${run.length}-backtick closing fence`);
|
|
128
|
-
addProse(projection, document.slice(proseStart, line.start));
|
|
129
|
-
const rawObject = parseJsonObject(document.slice(line.end, lines[closeIndex].start), `stored slot:${kind} fence`);
|
|
130
|
-
const { id, ...config } = rawObject;
|
|
131
|
-
if (id !== undefined && typeof id !== 'string')
|
|
132
|
-
throw new PageAuthoringError(`stored slot:${kind} fence has non-string reserved id`);
|
|
133
|
-
slots.push({ ...(id === undefined ? {} : { id }), kind, step: 0, config, ...(BUILTIN_PAGE_KINDS.includes(kind) ? {} : { unvalidated: true }) });
|
|
134
|
-
projection.push({ kind: 'slot', slotIndex: slots.length - 1 });
|
|
135
|
-
proseStart = lines[closeIndex].end;
|
|
136
|
-
index = closeIndex;
|
|
137
|
-
}
|
|
138
|
-
addProse(projection, document.slice(proseStart));
|
|
139
|
-
return { steps: 1, slots, projection };
|
|
140
|
-
}
|
|
141
|
-
const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
142
|
-
function tagEnd(html, start) {
|
|
143
|
-
let quote = null;
|
|
144
|
-
for (let index = start + 1; index < html.length; index++) {
|
|
145
|
-
const character = html[index];
|
|
146
|
-
if (quote !== null) {
|
|
147
|
-
if (character === quote)
|
|
148
|
-
quote = null;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (character === '"' || character === "'") {
|
|
152
|
-
quote = character;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (character === '>')
|
|
156
|
-
return index + 1;
|
|
157
|
-
}
|
|
158
|
-
throw new PageAuthoringError(`unterminated HTML tag beginning at byte ${start}`);
|
|
159
|
-
}
|
|
160
|
-
function parseAttributes(source) {
|
|
161
|
-
const attrs = new Map();
|
|
162
|
-
const tagNameEnd = source.search(/\s/);
|
|
163
|
-
const body = tagNameEnd === -1 ? '' : source.slice(tagNameEnd);
|
|
164
|
-
const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
165
|
-
for (const match of body.matchAll(pattern))
|
|
166
|
-
attrs.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4] ?? '');
|
|
167
|
-
return attrs;
|
|
168
|
-
}
|
|
169
|
-
function parseHtmlElements(html) {
|
|
170
|
-
const roots = [];
|
|
171
|
-
const all = [];
|
|
172
|
-
const stack = [];
|
|
173
|
-
let cursor = 0;
|
|
174
|
-
while (cursor < html.length) {
|
|
175
|
-
const start = html.indexOf('<', cursor);
|
|
176
|
-
if (start === -1)
|
|
177
|
-
break;
|
|
178
|
-
if (html.startsWith('<!--', start)) {
|
|
179
|
-
const end = html.indexOf('-->', start + 4);
|
|
180
|
-
if (end === -1)
|
|
181
|
-
throw new PageAuthoringError(`unterminated HTML comment at byte ${start}`);
|
|
182
|
-
cursor = end + 3;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
const end = tagEnd(html, start);
|
|
186
|
-
const token = html.slice(start, end);
|
|
187
|
-
if (/^<!/i.test(token) || /^<\?/i.test(token)) {
|
|
188
|
-
cursor = end;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
const closing = token.match(/^<\s*\/\s*([^\s>]+)/);
|
|
192
|
-
if (closing !== null) {
|
|
193
|
-
const tag = closing[1].toLowerCase();
|
|
194
|
-
let match = stack.length - 1;
|
|
195
|
-
while (match >= 0 && stack[match].tag !== tag)
|
|
196
|
-
match--;
|
|
197
|
-
if (match >= 0) {
|
|
198
|
-
const element = stack[match];
|
|
199
|
-
element.closeStart = start;
|
|
200
|
-
element.end = end;
|
|
201
|
-
stack.length = match;
|
|
202
|
-
}
|
|
203
|
-
cursor = end;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
const opening = token.match(/^<\s*([^\s/>]+)/);
|
|
207
|
-
if (opening === null) {
|
|
208
|
-
cursor = end;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
const tag = opening[1].toLowerCase();
|
|
212
|
-
const parent = stack.at(-1);
|
|
213
|
-
const element = { tag, attrs: parseAttributes(token.slice(1, -1)), start, openEnd: end, end, ...(parent === undefined ? {} : { parent }), children: [] };
|
|
214
|
-
if (parent === undefined)
|
|
215
|
-
roots.push(element);
|
|
216
|
-
else
|
|
217
|
-
parent.children.push(element);
|
|
218
|
-
all.push(element);
|
|
219
|
-
const selfClosing = /\/\s*>$/.test(token) || VOID_TAGS.has(tag);
|
|
220
|
-
if (tag === 'script' && !selfClosing) {
|
|
221
|
-
const closePattern = /<\s*\/\s*script\s*>/gi;
|
|
222
|
-
closePattern.lastIndex = end;
|
|
223
|
-
const close = closePattern.exec(html);
|
|
224
|
-
if (close === null)
|
|
225
|
-
throw new PageAuthoringError(`unterminated <script> element at byte ${start}`);
|
|
226
|
-
element.text = html.slice(end, close.index);
|
|
227
|
-
element.closeStart = close.index;
|
|
228
|
-
element.end = closePattern.lastIndex;
|
|
229
|
-
cursor = closePattern.lastIndex;
|
|
230
|
-
continue;
|
|
48
|
+
const errors = error.errors;
|
|
49
|
+
const first = errors?.[0];
|
|
50
|
+
if (first === undefined)
|
|
51
|
+
throw asPageAuthoringError(error);
|
|
52
|
+
const at = first.location == null ? '' : ` (page.tsx:${first.location.line}:${first.location.column})`;
|
|
53
|
+
throw new PageAuthoringError(`page does not compile: ${first.text ?? 'unknown error'}${at}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function parseTsx(document) {
|
|
57
|
+
return ts.createSourceFile('page.tsx', document, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX);
|
|
58
|
+
}
|
|
59
|
+
function eachNode(node, visit) {
|
|
60
|
+
visit(node);
|
|
61
|
+
node.forEachChild((child) => eachNode(child, visit));
|
|
62
|
+
}
|
|
63
|
+
/** Reject module plumbing: the compiled module is evaluated with no loader behind it,
|
|
64
|
+
* so any `import` or `require` could only fail at render time. */
|
|
65
|
+
function rejectModulePlumbing(sourceFile) {
|
|
66
|
+
let offense;
|
|
67
|
+
eachNode(sourceFile, (node) => {
|
|
68
|
+
if (offense !== undefined)
|
|
69
|
+
return;
|
|
70
|
+
if (ts.isImportDeclaration(node) || ts.isImportEqualsDeclaration(node))
|
|
71
|
+
offense = 'import';
|
|
72
|
+
else if (ts.isExportDeclaration(node) && node.moduleSpecifier !== undefined)
|
|
73
|
+
offense = 'export … from';
|
|
74
|
+
else if (ts.isCallExpression(node)) {
|
|
75
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword)
|
|
76
|
+
offense = 'dynamic import()';
|
|
77
|
+
else if (ts.isIdentifier(node.expression) && node.expression.text === 'require')
|
|
78
|
+
offense = 'require()';
|
|
231
79
|
}
|
|
232
|
-
if (!selfClosing)
|
|
233
|
-
stack.push(element);
|
|
234
|
-
cursor = end;
|
|
235
|
-
}
|
|
236
|
-
return all;
|
|
237
|
-
}
|
|
238
|
-
function ancestor(element, tag) {
|
|
239
|
-
let current = element.parent;
|
|
240
|
-
while (current !== undefined) {
|
|
241
|
-
if (current.tag === tag)
|
|
242
|
-
return current;
|
|
243
|
-
current = current.parent;
|
|
244
|
-
}
|
|
245
|
-
return undefined;
|
|
246
|
-
}
|
|
247
|
-
function decodeHtmlAttribute(value) {
|
|
248
|
-
return value.replace(/&(?:amp|quot|apos|lt|gt);|&#(?:x[0-9a-f]+|\d+);/gi, (entity) => {
|
|
249
|
-
const named = { '&': '&', '"': '"', ''': "'", '<': '<', '>': '>' };
|
|
250
|
-
const lower = entity.toLowerCase();
|
|
251
|
-
if (named[lower] !== undefined)
|
|
252
|
-
return named[lower];
|
|
253
|
-
const numeric = lower.startsWith('&#x') ? Number.parseInt(lower.slice(3, -1), 16) : Number.parseInt(lower.slice(2, -1), 10);
|
|
254
|
-
return Number.isFinite(numeric) ? String.fromCodePoint(numeric) : entity;
|
|
255
80
|
});
|
|
81
|
+
if (offense !== undefined)
|
|
82
|
+
throw new PageAuthoringError(`page source uses ${offense}: ${IMPORTLESS_SCOPE}`);
|
|
83
|
+
}
|
|
84
|
+
function stringProp(props, name) {
|
|
85
|
+
const value = props[name];
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (typeof value !== 'string')
|
|
89
|
+
throw new PageAuthoringError(`<Page> prop ${name} must be a string; it is part of the page manifest`);
|
|
90
|
+
return value;
|
|
256
91
|
}
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
throw new PageAuthoringError(`unknown page metadata element <meta name="${name}">`);
|
|
277
|
-
if (ancestor(element, 'head') === undefined)
|
|
278
|
-
throw new PageAuthoringError(`<meta name="${name}"> must appear inside <head>`);
|
|
279
|
-
const entries = recognized.get(name) ?? [];
|
|
280
|
-
entries.push(element);
|
|
281
|
-
recognized.set(name, entries);
|
|
282
|
-
}
|
|
283
|
-
for (const name of ['crtr-page-title', 'crtr-page-subtitle', 'crtr-page-placement']) {
|
|
284
|
-
const count = recognized.get(name)?.length ?? 0;
|
|
285
|
-
if ((name === 'crtr-page-title' && count !== 1) || (name !== 'crtr-page-title' && count > 1)) {
|
|
286
|
-
const expected = name === 'crtr-page-title' ? 'exactly one' : 'at most one';
|
|
287
|
-
throw new PageAuthoringError(`Expected ${expected} <meta name="${name}">, found ${count}.`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
const titleElement = recognized.get('crtr-page-title')[0];
|
|
291
|
-
const title = decodeHtmlAttribute(titleElement.attrs.get('content') ?? '').trim();
|
|
92
|
+
function scanPage(document, code, productComponents) {
|
|
93
|
+
rejectModulePlumbing(parseTsx(document));
|
|
94
|
+
const inScope = [...BUILTIN_PAGE_COMPONENT_TAGS, ...DISPLAY_PAGE_COMPONENT_TAGS, ...productComponents.map((component) => pageComponentTag(component.kind))];
|
|
95
|
+
const roots = evaluatePageTree(code, inScope);
|
|
96
|
+
const pages = [];
|
|
97
|
+
const collectPages = (nodes) => {
|
|
98
|
+
for (const node of nodes) {
|
|
99
|
+
if (node.tag === 'Page')
|
|
100
|
+
pages.push(node);
|
|
101
|
+
collectPages(node.children);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
collectPages(roots);
|
|
105
|
+
if (pages.length === 0)
|
|
106
|
+
throw new PageAuthoringError('page default export must render exactly one <Page title="…"> root; it rendered none');
|
|
107
|
+
if (pages.length > 1)
|
|
108
|
+
throw new PageAuthoringError(`page default export renders ${pages.length} <Page> elements; exactly one is allowed`);
|
|
109
|
+
const page = pages[0];
|
|
110
|
+
const title = (stringProp(page.props, 'title') ?? '').trim();
|
|
292
111
|
if (title === '')
|
|
293
|
-
throw new PageAuthoringError(
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
throw new PageAuthoringError(`<${element.tag}> at byte ${element.start} requires a direct <script type="application/json"> child`);
|
|
306
|
-
return parseJsonObject(script.text ?? '', `<${element.tag}> at byte ${element.start}`);
|
|
307
|
-
}
|
|
308
|
-
/** Project only body prose: slots remain terminal panels, while the pager's
|
|
309
|
-
* structural tags are invisible in the terminal. The raw fragments flow to
|
|
310
|
-
* the renderer, which downconverts HTML according to the manifest dialect. */
|
|
311
|
-
function htmlProjection(document, elements, slotElements) {
|
|
312
|
-
const body = elements.find((element) => element.tag === 'body');
|
|
313
|
-
if (body === undefined)
|
|
314
|
-
throw new PageAuthoringError('page document has no <body> element');
|
|
315
|
-
const bodyEnd = body.closeStart ?? document.length;
|
|
316
|
-
const excluded = slotElements.map((element) => ({ start: element.start, end: element.end }));
|
|
317
|
-
for (const element of elements) {
|
|
318
|
-
if (element.tag !== 'crtr-pages' && element.tag !== 'crtr-page')
|
|
319
|
-
continue;
|
|
320
|
-
excluded.push({ start: element.start, end: element.openEnd });
|
|
321
|
-
if (element.closeStart !== undefined)
|
|
322
|
-
excluded.push({ start: element.closeStart, end: element.end });
|
|
323
|
-
}
|
|
324
|
-
const ranges = excluded
|
|
325
|
-
.map((range) => ({ start: Math.max(range.start, body.openEnd), end: Math.min(range.end, bodyEnd) }))
|
|
326
|
-
.filter((range) => range.start < range.end)
|
|
327
|
-
.sort((left, right) => left.start - right.start || left.end - right.end);
|
|
328
|
-
const projection = [];
|
|
329
|
-
let cursor = body.openEnd;
|
|
330
|
-
for (const range of ranges) {
|
|
331
|
-
if (range.end <= cursor)
|
|
332
|
-
continue;
|
|
333
|
-
addProse(projection, document.slice(cursor, range.start));
|
|
334
|
-
const element = slotElements.find((slot) => slot.start === range.start && slot.end === range.end);
|
|
335
|
-
if (element !== undefined)
|
|
336
|
-
projection.push({ kind: 'slot', slotIndex: slotElements.indexOf(element) });
|
|
337
|
-
cursor = range.end;
|
|
338
|
-
}
|
|
339
|
-
addProse(projection, document.slice(cursor, bodyEnd));
|
|
340
|
-
return projection;
|
|
341
|
-
}
|
|
342
|
-
function scanHtml(document, productComponents) {
|
|
343
|
-
validateHtmlStructure(document);
|
|
344
|
-
const elements = parseHtmlElements(document);
|
|
345
|
-
const metadata = readHtmlMetadata(elements);
|
|
346
|
-
const wrappers = elements.filter((element) => element.tag === 'crtr-pages');
|
|
347
|
-
if (wrappers.some((candidate) => ancestor(candidate, 'crtr-pages') !== undefined))
|
|
348
|
-
throw new PageAuthoringError('nested <crtr-pages> element is not allowed');
|
|
349
|
-
if (wrappers.length > 1)
|
|
350
|
-
throw new PageAuthoringError(`more than one <crtr-pages> element: found ${wrappers.length}`);
|
|
351
|
-
const wrapper = wrappers[0];
|
|
352
|
-
const pages = elements.filter((element) => element.tag === 'crtr-page');
|
|
353
|
-
for (const page of pages)
|
|
354
|
-
if (ancestor(page, 'crtr-pages') !== wrapper || wrapper === undefined)
|
|
355
|
-
throw new PageAuthoringError(`<crtr-page> outside <crtr-pages> at byte ${page.start}`);
|
|
356
|
-
if (wrapper !== undefined && pages.length === 0)
|
|
357
|
-
throw new PageAuthoringError('empty <crtr-pages> element must contain at least one <crtr-page>');
|
|
358
|
-
const pageIndexes = new Map(pages.map((page, index) => [page, index]));
|
|
359
|
-
const rawSlots = [];
|
|
360
|
-
const slotElements = [];
|
|
361
|
-
for (const element of elements) {
|
|
362
|
-
if (element.tag === 'crtr-pages' || element.tag === 'crtr-page')
|
|
363
|
-
continue;
|
|
364
|
-
let kind;
|
|
365
|
-
let unvalidated = false;
|
|
366
|
-
if (element.tag === 'crtr-slot') {
|
|
367
|
-
kind = element.attrs.get('kind');
|
|
368
|
-
if (kind === undefined || !isRegisteredPageKind(kind, productComponents) || BUILTIN_PAGE_KINDS.includes(kind))
|
|
369
|
-
throw new PageAuthoringError(`<crtr-slot> at byte ${element.start} names unregistered product kind "${kind ?? ''}"`);
|
|
370
|
-
unvalidated = true;
|
|
371
|
-
}
|
|
372
|
-
else if (element.tag.startsWith('crtr-')) {
|
|
373
|
-
kind = element.tag.slice('crtr-'.length);
|
|
374
|
-
if (!BUILTIN_PAGE_KINDS.includes(kind))
|
|
375
|
-
throw new PageAuthoringError(`unregistered page element <${element.tag}> at byte ${element.start}; product kinds use <crtr-slot kind="…">`);
|
|
376
|
-
}
|
|
377
|
-
if (kind === undefined)
|
|
378
|
-
continue;
|
|
379
|
-
if (element.attrs.has('slot-id'))
|
|
380
|
-
throw new PageAuthoringError(`<${element.tag}> at byte ${element.start} has a slot-id attribute; author the slot id as id="…" — slot-id is written by the renderer`);
|
|
381
|
-
const id = element.attrs.get('id');
|
|
382
|
-
const containingPage = ancestor(element, 'crtr-page');
|
|
383
|
-
const step = containingPage === undefined ? 0 : pageIndexes.get(containingPage) ?? 0;
|
|
384
|
-
rawSlots.push({ ...(id === undefined ? {} : { id }), kind, step, config: configForHtmlElement(element), ...(unvalidated ? { unvalidated: true } : {}) });
|
|
385
|
-
slotElements.push(element);
|
|
112
|
+
throw new PageAuthoringError('<Page> requires a nonempty title prop');
|
|
113
|
+
const subtitle = stringProp(page.props, 'subtitle')?.trim();
|
|
114
|
+
if (page.props['placement'] !== undefined)
|
|
115
|
+
throw new PageAuthoringError('<Page> placement is not authored; choose --placement inline or --placement panel when sending the page.');
|
|
116
|
+
const productTags = new Map();
|
|
117
|
+
for (const component of productComponents) {
|
|
118
|
+
const tag = pageComponentTag(component.kind);
|
|
119
|
+
if (BUILTIN_PAGE_COMPONENT_TAGS.has(tag))
|
|
120
|
+
throw new PageAuthoringError(`product page component kind "${component.kind}" derives JSX tag "${tag}", which collides with a built-in page component`);
|
|
121
|
+
if (productTags.has(tag))
|
|
122
|
+
throw new PageAuthoringError(`product page component kind "${component.kind}" derives JSX tag "${tag}", which duplicates product page component kind "${productTags.get(tag)}"`);
|
|
123
|
+
productTags.set(tag, component.kind);
|
|
386
124
|
}
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
export function
|
|
433
|
-
|
|
434
|
-
const embed = slot.unvalidated === true ? undefined : SOURCE_EMBED[slot.kind];
|
|
435
|
-
const config = slot.config;
|
|
436
|
-
if (embed === undefined || typeof config.source !== 'string')
|
|
437
|
-
return slot;
|
|
438
|
-
const context = `slot ${slot.id === undefined ? `#${index}` : `"${slot.id}"`} (${slot.kind})`;
|
|
439
|
-
const raw = readSourceFile(config.source, context);
|
|
440
|
-
const parsed = embed.schema.safeParse(raw);
|
|
441
|
-
if (!parsed.success)
|
|
442
|
-
throw new PageAuthoringError(`${context} source data is invalid: ${issueText(parsed.error)}`);
|
|
443
|
-
const { source: _drop, ...rest } = config;
|
|
444
|
-
return { ...slot, config: { ...rest, [embed.field]: parsed.data } };
|
|
445
|
-
});
|
|
446
|
-
return validatePageManifest({ ...manifest, slots }, productComponents);
|
|
447
|
-
}
|
|
448
|
-
/** Replace every authored slot config with its stored, self-contained config for runtime rendering. */
|
|
449
|
-
export function embedPageSlotConfigs(document, manifest) {
|
|
450
|
-
const replacements = [];
|
|
451
|
-
let slotIndex = 0;
|
|
452
|
-
for (const element of parseHtmlElements(document)) {
|
|
453
|
-
const isSlot = element.tag === 'crtr-slot' || (element.tag.startsWith('crtr-') && element.tag !== 'crtr-pages' && element.tag !== 'crtr-page');
|
|
454
|
-
if (!isSlot)
|
|
455
|
-
continue;
|
|
456
|
-
const slot = manifest.slots[slotIndex];
|
|
457
|
-
if (slot === undefined)
|
|
458
|
-
throw new PageAuthoringError(`page document has more slots than its manifest at slot #${slotIndex}`);
|
|
459
|
-
const script = element.children.find((child) => child.tag === 'script' && child.attrs.get('type') === 'application/json');
|
|
460
|
-
if (script?.closeStart === undefined)
|
|
461
|
-
throw new PageAuthoringError(`slot #${slotIndex} has no JSON config script`);
|
|
462
|
-
replacements.push({ start: script.openEnd, end: script.closeStart, text: JSON.stringify(slot.config).replace(/</g, '\\u003c') });
|
|
463
|
-
slotIndex += 1;
|
|
464
|
-
}
|
|
465
|
-
if (slotIndex !== manifest.slots.length)
|
|
466
|
-
throw new PageAuthoringError(`page document has ${slotIndex} slots but manifest has ${manifest.slots.length}`);
|
|
467
|
-
return replacements.reduceRight((result, replacement) => `${result.slice(0, replacement.start)}${replacement.text}${result.slice(replacement.end)}`, document);
|
|
125
|
+
const slots = [];
|
|
126
|
+
let stepCount = 0;
|
|
127
|
+
const collectSlots = (nodes, step) => {
|
|
128
|
+
for (const node of nodes) {
|
|
129
|
+
const inStep = node.tag === 'Step' ? stepCount++ : step;
|
|
130
|
+
const builtin = BUILTIN_KIND_BY_TAG[node.tag];
|
|
131
|
+
const product = productTags.get(node.tag);
|
|
132
|
+
if (builtin !== undefined || product !== undefined) {
|
|
133
|
+
const { id, ...config } = jsonProps(node.props);
|
|
134
|
+
if (id !== undefined && typeof id !== 'string')
|
|
135
|
+
throw new PageAuthoringError(`<${node.tag}> id prop must be a string`);
|
|
136
|
+
slots.push({
|
|
137
|
+
...(id === undefined ? {} : { id }),
|
|
138
|
+
kind: builtin ?? product,
|
|
139
|
+
step: inStep,
|
|
140
|
+
config,
|
|
141
|
+
...(builtin === undefined ? { unvalidated: true } : {}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
collectSlots(node.children, inStep);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
collectSlots(page.children, 0);
|
|
148
|
+
return { title, ...(subtitle === undefined || subtitle === '' ? {} : { subtitle }), steps: Math.max(stepCount, 1), slots };
|
|
149
|
+
}
|
|
150
|
+
/** Derive the transport manifest from one authored TSX page document. */
|
|
151
|
+
export function derivePageManifestFromDocument(document, productComponents = [], source, delivery = { placement: 'inline', inbox: false, reply: false }) {
|
|
152
|
+
const scanned = scanPage(document, compilePage(document), productComponents);
|
|
153
|
+
return validateAuthoredManifest({ schema: 'crtr.page/v2', dialect: 'jsx', title: scanned.title, ...(scanned.subtitle === undefined ? {} : { subtitle: scanned.subtitle }), delivery, document: 'page.tsx', steps: scanned.steps, slots: scanned.slots, ...(source === undefined ? {} : { source }) }, productComponents);
|
|
154
|
+
}
|
|
155
|
+
function htmlMetadata(document) {
|
|
156
|
+
const title = /<title\b[^>]*>([\s\S]*?)<\/title\s*>/i.exec(document)?.[1]?.replace(/<[^>]+>/g, '').trim();
|
|
157
|
+
if (title === undefined || title === '')
|
|
158
|
+
throw new PageAuthoringError('HTML page requires a nonempty <title>.');
|
|
159
|
+
const description = [...document.matchAll(/<meta\b[^>]*>/gi)].map((match) => match[0]).map((tag) => {
|
|
160
|
+
const attrs = new Map([...tag.matchAll(/\b([^\s=/>]+)\s*=\s*(["'])([\s\S]*?)\2/g)].map((match) => [match[1].toLowerCase(), match[3]]));
|
|
161
|
+
return attrs.get('name')?.toLowerCase() === 'description' ? attrs.get('content')?.trim() : undefined;
|
|
162
|
+
}).find((value) => value !== undefined);
|
|
163
|
+
return { title, ...(description === undefined || description === '' ? {} : { subtitle: description }) };
|
|
164
|
+
}
|
|
165
|
+
export function deriveHtmlPageManifest(document, source, delivery = { placement: 'inline', inbox: false, reply: false }) {
|
|
166
|
+
const metadata = htmlMetadata(document);
|
|
167
|
+
return validateAuthoredManifest({ schema: 'crtr.page/v2', dialect: 'html', ...metadata, delivery, document: 'page.html', steps: 1, slots: [], ...(source === undefined ? {} : { source }) }, []);
|
|
168
|
+
}
|
|
169
|
+
/** Derive the transport manifest from one authored TSX page source file. */
|
|
170
|
+
export function derivePageManifest(sourceFile, productComponents = [], source, delivery) {
|
|
171
|
+
return derivePageManifestFromDocument(readFileSync(sourceFile, 'utf8'), productComponents, source, delivery);
|
|
468
172
|
}
|
|
469
173
|
/** Read and validate page.json from a ticket directory. */
|
|
470
|
-
export function parsePage(dir
|
|
174
|
+
export function parsePage(dir) {
|
|
471
175
|
let raw;
|
|
472
176
|
try {
|
|
473
177
|
raw = JSON.parse(readFileSync(join(dir, 'page.json'), 'utf8'));
|
|
@@ -475,41 +179,5 @@ export function parsePage(dir, productComponents = []) {
|
|
|
475
179
|
catch {
|
|
476
180
|
throw new PageAuthoringError('page.json is not valid JSON');
|
|
477
181
|
}
|
|
478
|
-
return validatePageManifest(raw
|
|
479
|
-
}
|
|
480
|
-
/** Key-order-independent value identity, so a re-scan compares by content. */
|
|
481
|
-
function canonicalJson(value) {
|
|
482
|
-
if (value === null || typeof value !== 'object')
|
|
483
|
-
return JSON.stringify(value) ?? 'null';
|
|
484
|
-
if (Array.isArray(value))
|
|
485
|
-
return `[${value.map(canonicalJson).join(',')}]`;
|
|
486
|
-
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => (left < right ? -1 : 1));
|
|
487
|
-
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
|
|
488
|
-
}
|
|
489
|
-
function comparableConfig(kind, config, sourceProjection) {
|
|
490
|
-
const embed = SOURCE_EMBED[kind];
|
|
491
|
-
if (embed === undefined || !sourceProjection)
|
|
492
|
-
return config;
|
|
493
|
-
const { source: _source, [embed.field]: _data, ...rest } = config;
|
|
494
|
-
return rest;
|
|
495
|
-
}
|
|
496
|
-
/** Project authored source for local rendering and verify its slot references against the manifest. */
|
|
497
|
-
export function parsePageSource(document, manifest, productComponents = []) {
|
|
498
|
-
const canonical = validatePageManifest(manifest, productComponents);
|
|
499
|
-
const scanned = canonical.dialect === 'md'
|
|
500
|
-
? projectLegacyMarkdownSource(document, productComponents)
|
|
501
|
-
: scanHtml(document, productComponents);
|
|
502
|
-
// Normalize the re-scan through the same schema pass the manifest went through,
|
|
503
|
-
// so per-kind config defaults are on both sides of the comparison.
|
|
504
|
-
const derived = validateAuthoredPageManifest({ schema: 'crtr.page/v1', dialect: canonical.dialect, title: canonical.title, ...(canonical.subtitle === undefined ? {} : { subtitle: canonical.subtitle }), placement: canonical.placement, document: canonical.document, steps: scanned.steps, slots: scanned.slots }, productComponents);
|
|
505
|
-
if (derived.slots.length !== canonical.slots.length)
|
|
506
|
-
throw new PageAuthoringError(`page source has ${derived.slots.length} slots but manifest has ${canonical.slots.length}`);
|
|
507
|
-
for (let index = 0; index < canonical.slots.length; index++) {
|
|
508
|
-
const left = derived.slots[index];
|
|
509
|
-
const right = canonical.slots[index];
|
|
510
|
-
const sourceProjection = typeof left.config.source === 'string';
|
|
511
|
-
if (canonicalJson({ ...left, config: comparableConfig(left.kind, left.config, sourceProjection) }) !== canonicalJson({ ...right, config: comparableConfig(right.kind, right.config, sourceProjection) }))
|
|
512
|
-
throw new PageAuthoringError(`page source slot #${index} (${left.id ?? left.kind}) does not match manifest slot #${index}`);
|
|
513
|
-
}
|
|
514
|
-
return scanned.projection;
|
|
182
|
+
return validatePageManifest(raw);
|
|
515
183
|
}
|