@cosmicdrift/kumiko-samples 0.362.0 → 0.363.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/package.json +1 -1
- package/packages/bundled-features/package.json +17 -6
- package/packages/bundled-features/src/admin-shell/changes.json +6 -0
- package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
- package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +14 -0
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
- package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
- package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
- package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
- package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
- package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
- package/packages/bundled-features/src/channel-email/changes.json +19 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
- package/packages/bundled-features/src/channel-email/feature.ts +6 -3
- package/packages/bundled-features/src/channel-email/index.ts +7 -1
- package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
- package/packages/bundled-features/src/channel-email/types.ts +23 -0
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
- package/packages/bundled-features/src/data-retention/changes.json +6 -0
- package/packages/bundled-features/src/data-retention/index.ts +1 -0
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
- package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
- package/packages/bundled-features/src/delivery/changes.json +48 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
- package/packages/bundled-features/src/delivery/constants.ts +2 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +17 -2
- package/packages/bundled-features/src/delivery/jobs.ts +48 -5
- package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
- package/packages/bundled-features/src/delivery/testing.ts +9 -1
- package/packages/bundled-features/src/delivery/types.ts +28 -18
- package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
- package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
- package/packages/bundled-features/src/file-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
- package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
- package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
- package/packages/bundled-features/src/ledger/recurring.ts +1 -3
- package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
- package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
- package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
- package/packages/bundled-features/src/page-render/branding.ts +3 -0
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
- package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
- package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
- package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
- package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
- package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
- package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
- package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/sessions/feature.ts +2 -4
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
- package/packages/bundled-features/src/shared/index.ts +6 -0
- package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
- package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
- package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
- package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
- package/packages/bundled-features/src/template-resolver/README.md +63 -7
- package/packages/bundled-features/src/template-resolver/api.ts +83 -8
- package/packages/bundled-features/src/template-resolver/changes.json +118 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +100 -0
- package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
- package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
- package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
- package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
- package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +14 -0
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
- package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
- package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
- package/packages/bundled-features/src/tenant/seeding.ts +41 -7
- package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
- package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
- package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
- package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
- package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
- package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +7 -0
- package/packages/bundled-features/src/waitlist/feature.ts +17 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
- package/packages/bundled-features/src/waitlist/mail.ts +69 -18
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
- package/samples/apps/ui-walkthrough/package.json +2 -0
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
- package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
- package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
- package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
- package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +2 -0
- package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
- package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
- package/samples/recipes/delivery-notifications/README.md +26 -25
- package/samples/recipes/delivery-notifications/package.json +2 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
- package/samples/recipes/seed-migration/README.md +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
- package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
- package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
- package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# template-resolver
|
|
2
2
|
|
|
3
|
-
Strukturierter Content-Storage mit Tenant-Override-Hierarchie, Locale-Fallback und Resource-Linking via `file-foundation`. Eine Entity (`template-resource`) für alle Content-Arten — Render-Templates (`mail
|
|
3
|
+
Strukturierter Content-Storage mit Tenant-Override-Hierarchie, Locale-Fallback und Resource-Linking via `file-foundation`. Eine Entity (`template-resource`) für alle Content-Arten — Render-Templates (`mail`, `document-pdf`, `notification`, `image-snapshot`), Rahmen (`mail-layout`, `pdf-layout`), Markdown-Textblöcke (`text-block`, ex `text-content`-Feature) und AI-Prompts (`ai-prompt`).
|
|
4
4
|
|
|
5
5
|
**Plan-Doc:** [`kumiko-platform/docs/plans/features/template-resolver.md`](../../../../../../kumiko-platform/docs/plans/features/template-resolver.md)
|
|
6
6
|
|
|
@@ -38,14 +38,51 @@ async function someHandler(ctx) {
|
|
|
38
38
|
const template = await templateResolver.resolveTemplate({
|
|
39
39
|
tenantId: ctx.user.tenantId,
|
|
40
40
|
slug: "nka-versand",
|
|
41
|
-
kind: "mail
|
|
41
|
+
kind: "mail",
|
|
42
42
|
locale: "de",
|
|
43
43
|
});
|
|
44
|
-
// template.content + template.variableSchema + template.linkedResources verwenden
|
|
44
|
+
// template.content + template.meta + template.variableSchema + template.linkedResources verwenden
|
|
45
45
|
// ...
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## Kinds, Format und Meta
|
|
50
|
+
|
|
51
|
+
| kind | contentFormat | meta |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `mail` | `html` (Dokument-Schema, wird serverseitig neu aufgebaut) | Pflicht: `subject`; optional `previewText`, `layoutSlug`, `category` (`transactional`/`marketing`), `sender` |
|
|
54
|
+
| `mail-layout` | `mjml` | leer (`{}`) oder fehlend |
|
|
55
|
+
| `pdf-layout` | `html` | leer (`{}`) oder fehlend |
|
|
56
|
+
| `document-pdf` | `html` | fehlt |
|
|
57
|
+
| übrige | frei | fehlt |
|
|
58
|
+
|
|
59
|
+
`meta` liegt als JSON in der Spalte `meta` und wird beim Schreiben pro kind gegen
|
|
60
|
+
`template-meta.ts` geprüft (Fehler als Field-Issues auf `meta.*`). Subject und
|
|
61
|
+
Preview-Text erlauben nur einfache Platzhalter (`{{name}}`, `{{name.feld}}`), keine
|
|
62
|
+
Zeilenumbrüche.
|
|
63
|
+
|
|
64
|
+
Slugs sind kebab-Segmente, mit `:` verbunden (`templateSlugSchema`); Notification-Templates
|
|
65
|
+
liegen unter ihrem qualifizierten Namen `feature:notify:name`.
|
|
66
|
+
|
|
67
|
+
## Platzhalter-Validierung (kind `mail`)
|
|
68
|
+
|
|
69
|
+
Die Variablen kommen aus der Notification mit gleichem qualifiziertem Namen
|
|
70
|
+
(`r.notification({ variables })`), sonst aus `variableSchema` der Zeile. Systemplatzhalter
|
|
71
|
+
(`tenantName`, `signature`, `imprint`, `unsubscribeUrl`) und der Resource-Key `logo` sind immer erlaubt.
|
|
72
|
+
|
|
73
|
+
- `upsertSystem` und `upsertTenant` lehnen unbekannte Platzhalter ab (Field-Issue auf `content`).
|
|
74
|
+
- `upsertSystem` und `upsertTenant` mit `status: "active"` verlangen zusätzlich, dass jede `required`-Variable benutzt wird. Ein Tenant-Draft darf unvollständig sein und liefert `validation` zurück.
|
|
75
|
+
- `publish` prüft die gespeicherte Zeile erneut.
|
|
76
|
+
- `seedSystemTemplate` wirft bei beiden Problemen.
|
|
77
|
+
|
|
78
|
+
## Notification-Templates seeden
|
|
79
|
+
|
|
80
|
+
`r.notification({ variables, template, meta })` bringt Text und Meta mit dem Code mit.
|
|
81
|
+
`seedNotificationTemplates(db, registry)` (aus `.../template-resolver/seeding`) schreibt sie
|
|
82
|
+
pro Locale als System-Template; `applyBootSeeds` ruft das beim Boot auf. Der Code ist
|
|
83
|
+
maßgeblich: geänderte Texte aktualisieren die Zeile, unveränderte erzeugen kein Event.
|
|
84
|
+
Tenant-Overrides bleiben unberührt.
|
|
85
|
+
|
|
49
86
|
## Resolver-Reihenfolge (4-Stufen-Fallback)
|
|
50
87
|
|
|
51
88
|
1. `tenantId` + requested locale
|
|
@@ -63,10 +100,10 @@ import { seedSystemTemplate } from "@cosmicdrift/kumiko-bundled-features/templat
|
|
|
63
100
|
// runProdApp seeds — idempotent, default skip if slug exists
|
|
64
101
|
await seedSystemTemplate(db, {
|
|
65
102
|
slug: "welcome-email",
|
|
66
|
-
kind: "
|
|
103
|
+
kind: "mail",
|
|
67
104
|
locale: "de",
|
|
68
|
-
content:
|
|
69
|
-
contentFormat: "
|
|
105
|
+
content: '<p>Willkommen, <span data-variable="name"></span></p>',
|
|
106
|
+
contentFormat: "html",
|
|
70
107
|
});
|
|
71
108
|
```
|
|
72
109
|
|
|
@@ -80,8 +117,16 @@ await seedSystemTemplate(db, {
|
|
|
80
117
|
| `TemplateResolverHandlers.upsertTenant` | `template-resolver:write:upsert-tenant` | TenantAdmin (eigener Tenant) + SystemAdmin via `tenantIdOverride` | Erstellt/Updated Tenant-Overrides (scope='tenant'), default-status='draft' |
|
|
81
118
|
| `TemplateResolverHandlers.publish` | `template-resolver:write:publish` | TenantAdmin (eigener Tenant) | Setzt status='active' |
|
|
82
119
|
| `TemplateResolverHandlers.archive` | `template-resolver:write:archive` | TenantAdmin (eigener Tenant) | Setzt status='archived' (Resolver ignoriert es danach) |
|
|
120
|
+
| `TemplateResolverHandlers.publishSystem` | `template-resolver:write:publish-system` | SystemAdmin | Setzt status='active' auf einem System-Default (gleiche Mail-Prüfung wie `publish`) |
|
|
121
|
+
| `TemplateResolverHandlers.archiveSystem` | `template-resolver:write:archive-system` | SystemAdmin | Setzt status='archived' auf einem System-Default; Tenants ohne Override fallen durch |
|
|
122
|
+
| `TemplateResolverHandlers.resetToSystem` | `template-resolver:write:reset-to-system` | TenantAdmin (eigener Tenant) | Archiviert den eigenen Override, sodass wieder die aktive System-Vorlage greift; ohne aktive System-Vorlage `no_system_template` |
|
|
83
123
|
| `TemplateResolverQueries.findById` | `template-resolver:query:find-by-id` | TenantAdmin + User (eigener Tenant + system-templates sichtbar) | Raw-Lookup für Edit-UI |
|
|
84
124
|
| `TemplateResolverQueries.list` | `template-resolver:query:list` | gleich | Filter nach kind/locale/status, optional includeSystem |
|
|
125
|
+
| `TemplateResolverQueries.usage` | `template-resolver:query:usage` | TenantAdmin | Welche Notifications eine Mail-Vorlage bzw. ein Mail-Layout verwenden (aus der Registry) |
|
|
126
|
+
| `TemplateResolverQueries.overview` | `template-resolver:query:overview` | TenantAdmin + SystemAdmin | Eine Zeile je Slug und Art (Mail, PDF, Layouts) mit Herkunft, Sprachen, Status, Version, Layout und Verwendung; durchsuch-, filter-, sortier- und seitenweise lesbar |
|
|
127
|
+
| `TemplateResolverQueries.layoutSource` | `template-resolver:query:layout-source` | TenantAdmin + SystemAdmin | Eigene und System-Fassung eines Layouts, Kopf/Signatur/Fuß-Bereiche der wirksamen html-Fassung, Speicher-Locale; MJML-Quelle nur für SystemAdmin |
|
|
128
|
+
| `TemplateResolverQueries.tenantOverrides` | `template-resolver:query:tenant-overrides` | SystemAdmin | Anzahl aktiver/Entwurf-Overrides einer System-Vorlage und aktive Tenants gesamt, nur Zahlen |
|
|
129
|
+
| `TemplateResolverQueries.diffToSystem` | `template-resolver:query:diff-to-system` | TenantAdmin (eigener Tenant) | Block- und Meta-Vergleich eines Tenant-Overrides mit der System-Vorlage |
|
|
85
130
|
|
|
86
131
|
**SystemAdmin-Cross-Tenant für publish/archive/findById:** deferred (Task 8) — `upsertTenant` hat `tenantIdOverride` bereits; publish/archive brauchen das erst wenn ein Sysadmin-Panel fremde Tenant-Templates kuratiert. `ctx.db` ist tenant-scoped, fremde IDs → NotFound ohne Override.
|
|
87
132
|
|
|
@@ -162,7 +207,7 @@ createTemplateResolverFeature({
|
|
|
162
207
|
collections: [
|
|
163
208
|
{
|
|
164
209
|
id: "reply-snippets",
|
|
165
|
-
kind: "mail
|
|
210
|
+
kind: "mail",
|
|
166
211
|
access: { roles: ["Agent", "TenantAdmin"] },
|
|
167
212
|
nav: { label: "mail:nav.snippets", parent: "mail:nav:root" },
|
|
168
213
|
},
|
|
@@ -245,6 +290,17 @@ nebeneinander und führt sie in der UI zusammen.
|
|
|
245
290
|
Unter `tenantIdOverride` (SystemAdmin) bleibt die Zeile die **eigene** des
|
|
246
291
|
Aufrufers im fremden Tenant — ein Admin editiert nie die Signatur eines anderen.
|
|
247
292
|
|
|
293
|
+
## Validierung, Layouts, Bundles
|
|
294
|
+
|
|
295
|
+
- `validate` (Query) trocken-läuft die Schreib-Pipeline: `canSave`, `canPublish`, verwendete/unbekannte/fehlende Platzhalter, verfügbare Variablen. `mail`, `document-pdf`, `pdf-layout` und `mail-layout` laufen durch dasselbe Platzhalter-Gate; `page`/`pages` gibt es nur in `document-pdf` und `pdf-layout`.
|
|
296
|
+
- `preview` (Query, TenantAdmin/SystemAdmin) rendert einen Entwurf oder, ohne `content`, die gespeicherte Vorlage mit Beispielwerten aus dem `example` jeder deklarierten Variable (Listen: zwei Zeilen, ohne `example` feste Ersatzwerte). Der Entwurf läuft durch dieselben Gates wie ein Speichern, eine Mail durch den echten Mail-Renderer. Antwort: Betreff, HTML, `displayHtml` (nur Mail: `cid:`-Bilder als Data-URI für den Editor, 5 MiB Budget), Text, Anhänge (`kind: "inline"` mit `cid` oder `kind: "file"`, jeweils mit Größe) und `checks` (`resources_resolved`, `text_present`, `size_under_cap` mit 10 MiB, `required_placeholders`). `document-pdf` liefert den bereinigten Body und die Layout-Regionen, kein PDF.
|
|
297
|
+
- `send-test` (Write, TenantAdmin/SystemAdmin) schickt die gerenderte Mail nur an die eigene Adresse des Aufrufers, Betreff mit `[TEST] `, höchstens 10 pro Nutzer und Stunde. Es gibt keine Empfänger-Eingabe. Braucht `delivery`, `channel-email` und `renderer-mail-html`.
|
|
298
|
+
- Layouts: Tenant-`mail-layout` und `pdf-layout` sind strukturiertes HTML mit `data-content`-Slot und Regionen (Header, Signatur, Footer). Rohes MJML mit `{{{content}}}` genau einmal darf nur das System schreiben.
|
|
299
|
+
- `export-bundle`, `import-bundle`, `import-bundle-system`: ein Template mit allen Locales und Ressourcen-Dateien (base64) als eigenständiges Bundle; bestehende Ziele nur mit `overwrite`. Systemimport mit Ressourcen braucht einen Caller im System-Tenant.
|
|
300
|
+
- `copy-locale`: Startpunkt für Übersetzungen, kopiert als Tenant-Draft. Mit `scope: "system"` (nur SystemAdmin) wird eine Systemvorlage als System-Draft kopiert; andere Aufrufer bekommen 403.
|
|
301
|
+
- Mail-Anhänge: `resource-upload` mit `purpose: "attachment"` und `fileName` legt eine PDF-, PNG- oder JPEG-Datei (geprüft am Inhalt, Originalbytes, höchstens 10 pro Mail-Vorlage) neben den Inline-Bildern ab. `resource-remove`, `copy-locale` und die Bundles tragen sie mit (Bundle-Eintrag `purpose: "attachment"`). Der Mail-Renderer hängt sie nach den Inline-Bildern an.
|
|
302
|
+
- `r.notification(..., { record: { entity, map } })` mappt eine Entity-Zeile auf die deklarierten Variablen (`mapRecordToVariables`); Boot prüft Entity, Variablen und Feldpfade.
|
|
303
|
+
|
|
248
304
|
## Out-of-Scope
|
|
249
305
|
|
|
250
306
|
- Rendering (Markdown/MJML → HTML/PDF) — siehe `renderer-foundation`
|
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
// ctx.templateResolver at runtime.
|
|
4
4
|
|
|
5
5
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
6
|
-
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
6
|
+
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
7
7
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
8
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
9
9
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
10
|
+
import type { FileAttachmentRef } from "../renderer-foundation/index.js";
|
|
10
11
|
import type { ContentFormat, TemplateKind } from "./constants.js";
|
|
11
12
|
import { FALLBACK_LOCALE, SYSTEM_TENANT_ID } from "./constants.js";
|
|
13
|
+
import {
|
|
14
|
+
fileAttachmentsOf,
|
|
15
|
+
imageLinksOf,
|
|
16
|
+
parseLinkedResourceEntries,
|
|
17
|
+
pdfAttachmentLinksOf,
|
|
18
|
+
type TemplatePdfAttachmentLink,
|
|
19
|
+
} from "./linked-resources.js";
|
|
12
20
|
import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
|
|
21
|
+
import { parseTemplateMeta, type TemplateMeta } from "./template-meta.js";
|
|
13
22
|
|
|
14
23
|
// Public TemplateResource: what consumers see. Hides DB-internal columns
|
|
15
24
|
// (insertedById, internal id-type) but keeps the fields needed for rendering.
|
|
@@ -25,7 +34,15 @@ export type TemplateResource = {
|
|
|
25
34
|
readonly content: string;
|
|
26
35
|
readonly contentFormat: ContentFormat;
|
|
27
36
|
readonly variableSchema: Record<string, unknown>;
|
|
37
|
+
/** Embeddable images: resource key -> fileRef id. */
|
|
28
38
|
readonly linkedResources: Record<string, string>;
|
|
39
|
+
/** Files attached to every mail rendered from this template, ordered by key. */
|
|
40
|
+
readonly fileAttachments?: readonly FileAttachmentRef[];
|
|
41
|
+
/** PDFs rendered from document templates and attached at send time, ordered by key. */
|
|
42
|
+
readonly pdfAttachmentLinks?: readonly TemplatePdfAttachmentLink[];
|
|
43
|
+
/** Parsed per kind; null when absent or when the stored JSON no longer
|
|
44
|
+
* matches the kind's schema (the renderer then fails with a clear error). */
|
|
45
|
+
readonly meta: TemplateMeta | null;
|
|
29
46
|
readonly scope: "system" | "tenant";
|
|
30
47
|
readonly parentTemplateId: string | null;
|
|
31
48
|
readonly status: "draft" | "active" | "archived";
|
|
@@ -39,6 +56,15 @@ export type ResolveRequest = {
|
|
|
39
56
|
readonly locale: string;
|
|
40
57
|
};
|
|
41
58
|
|
|
59
|
+
export type LayoutKind = "mail-layout" | "pdf-layout";
|
|
60
|
+
|
|
61
|
+
export type ResolveLayoutRequest = {
|
|
62
|
+
readonly tenantId: TenantId;
|
|
63
|
+
readonly slug: string;
|
|
64
|
+
readonly kind: LayoutKind;
|
|
65
|
+
readonly locale: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
42
68
|
export type TemplateResolverApi = {
|
|
43
69
|
/**
|
|
44
70
|
* Findet ein konkretes Template by (tenantId, slug, kind, locale).
|
|
@@ -74,9 +100,20 @@ export type TemplateResolverApi = {
|
|
|
74
100
|
* User cross-tenant Templates abfragen (Tenant-Isolation gebrochen).
|
|
75
101
|
*/
|
|
76
102
|
readonly resolveTemplate: (args: ResolveRequest) => Promise<TemplateResource>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Layouts are locale-neutral in the product (the layout editor saves under the locale of the
|
|
106
|
+
* row it loaded), so the locale chain is wider than resolveTemplate: tenant (requested,
|
|
107
|
+
* FALLBACK_LOCALE, any active locale), then system (same). Only active rows count; among
|
|
108
|
+
* "any" the highest version wins, then the alphabetically first locale, so the pick is stable.
|
|
109
|
+
* Throws TemplateNotFoundError when neither scope has an active row.
|
|
110
|
+
*
|
|
111
|
+
* **Caller invariant:** `tenantId` must come from the server, as for `resolveTemplate`.
|
|
112
|
+
*/
|
|
113
|
+
readonly resolveLayout: (args: ResolveLayoutRequest) => Promise<TemplateResource>;
|
|
77
114
|
};
|
|
78
115
|
|
|
79
|
-
export function createTemplateResolverApi(db: DbRunner): TemplateResolverApi {
|
|
116
|
+
export function createTemplateResolverApi(db: DbRunner | TenantDb): TemplateResolverApi {
|
|
80
117
|
return {
|
|
81
118
|
findExact: async ({ tenantId, slug, kind, locale, scope }) => {
|
|
82
119
|
const effectiveTenantId = scope === "system" ? SYSTEM_TENANT_ID : tenantId;
|
|
@@ -102,11 +139,39 @@ export function createTemplateResolverApi(db: DbRunner): TemplateResolverApi {
|
|
|
102
139
|
}
|
|
103
140
|
throw new TemplateNotFoundError({ slug, kind, locale });
|
|
104
141
|
},
|
|
142
|
+
|
|
143
|
+
resolveLayout: async ({ tenantId, slug, kind, locale }) => {
|
|
144
|
+
for (const tid of [tenantId, SYSTEM_TENANT_ID]) {
|
|
145
|
+
const rows = await selectMany<TemplateResourceRow>(db, templateResourcesTable, {
|
|
146
|
+
tenantId: tid,
|
|
147
|
+
slug,
|
|
148
|
+
kind,
|
|
149
|
+
});
|
|
150
|
+
const row = pickActiveLayoutRow(rows, locale);
|
|
151
|
+
if (row !== null) return toPublic(row);
|
|
152
|
+
}
|
|
153
|
+
throw new TemplateNotFoundError({ slug, kind, locale });
|
|
154
|
+
},
|
|
105
155
|
};
|
|
106
156
|
}
|
|
107
157
|
|
|
158
|
+
function pickActiveLayoutRow(
|
|
159
|
+
rows: readonly TemplateResourceRow[],
|
|
160
|
+
locale: string,
|
|
161
|
+
): TemplateResourceRow | null {
|
|
162
|
+
const active = rows
|
|
163
|
+
.filter((row) => row.status === "active")
|
|
164
|
+
.sort((a, b) => b.version - a.version || a.locale.localeCompare(b.locale));
|
|
165
|
+
return (
|
|
166
|
+
active.find((row) => row.locale === locale) ??
|
|
167
|
+
active.find((row) => row.locale === FALLBACK_LOCALE) ??
|
|
168
|
+
active[0] ??
|
|
169
|
+
null
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
108
173
|
async function fetchTemplate(
|
|
109
|
-
db: DbRunner,
|
|
174
|
+
db: DbRunner | TenantDb,
|
|
110
175
|
tenantId: string,
|
|
111
176
|
slug: string,
|
|
112
177
|
kind: TemplateKind,
|
|
@@ -127,7 +192,6 @@ function toPublic(row: TemplateResourceRow): TemplateResource {
|
|
|
127
192
|
// @cast-boundary db-row — Drizzle-Schema typisiert kind/contentFormat/
|
|
128
193
|
// scope/status als generic text. CHECK-Constraints in der DB schränken
|
|
129
194
|
// sie auf die Union-Types ein; Cast assertet das Schema-Wissen.
|
|
130
|
-
// linkedResources ist ein text-column mit JSON-payload (string→string map).
|
|
131
195
|
const kind = row.kind as TemplateKind;
|
|
132
196
|
// @cast-boundary db-row — siehe kind.
|
|
133
197
|
const contentFormat = row.contentFormat as ContentFormat;
|
|
@@ -135,9 +199,7 @@ function toPublic(row: TemplateResourceRow): TemplateResource {
|
|
|
135
199
|
const scope = row.scope as "system" | "tenant";
|
|
136
200
|
// @cast-boundary db-row — siehe kind.
|
|
137
201
|
const status = row.status as "draft" | "active" | "archived";
|
|
138
|
-
|
|
139
|
-
// linkedResources-Spalte enthält per Schema {key: signedUrl}-Map.
|
|
140
|
-
const linkedResources = parseJson(row.linkedResources) as Record<string, string>;
|
|
202
|
+
const linkedEntries = parseLinkedResourceEntries(row.linkedResources);
|
|
141
203
|
return {
|
|
142
204
|
id: String(row.id),
|
|
143
205
|
version: row.version,
|
|
@@ -150,7 +212,10 @@ function toPublic(row: TemplateResourceRow): TemplateResource {
|
|
|
150
212
|
content: row.content ?? "",
|
|
151
213
|
contentFormat,
|
|
152
214
|
variableSchema: parseJson(row.variableSchema),
|
|
153
|
-
linkedResources,
|
|
215
|
+
linkedResources: imageLinksOf(linkedEntries),
|
|
216
|
+
fileAttachments: fileAttachmentsOf(linkedEntries),
|
|
217
|
+
pdfAttachmentLinks: pdfAttachmentLinksOf(linkedEntries),
|
|
218
|
+
meta: parseStoredMeta(kind, row.meta),
|
|
154
219
|
scope,
|
|
155
220
|
parentTemplateId: row.parentTemplateId,
|
|
156
221
|
status,
|
|
@@ -158,6 +223,16 @@ function toPublic(row: TemplateResourceRow): TemplateResource {
|
|
|
158
223
|
};
|
|
159
224
|
}
|
|
160
225
|
|
|
226
|
+
function parseStoredMeta(kind: TemplateKind, raw: string | null): TemplateMeta | null {
|
|
227
|
+
if (!raw) return null;
|
|
228
|
+
try {
|
|
229
|
+
const result = parseTemplateMeta(kind, JSON.parse(raw));
|
|
230
|
+
return result.ok ? result.meta : null;
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
161
236
|
function parseJson(raw: string | null): Record<string, unknown> {
|
|
162
237
|
if (!raw) return {};
|
|
163
238
|
try {
|
|
@@ -1,4 +1,122 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.363.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Overview and layout-source queries for the template designer",
|
|
6
|
+
"detail": "template-resolver:query:overview lists the mail, PDF and layout templates of the caller's tenant grouped by slug and kind (effective origin, locales, status, versions, layout slug, last change and the notifications that use it) and template-resolver:query:layout-source returns the header, signature and footer regions of a mail or PDF layout plus the locale a save has to use; a SystemAdmin also receives the MJML source of the system layout. The usage lookup is the exported collectTemplateUsage."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.363.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "contentFormat \"document\" with a toolbar editor for mail and PDF templates",
|
|
12
|
+
"detail": "ContentEditorFormat gains \"document\". template-resolver registers DocumentContentEditor for it (tiptap, lazy-loaded) and stores the body as html. The toolbar offers paragraph style, bold, italic, lists, links, placeholders and the blocks image, table, button, divider, repeat and page break; the blocks prop picks them (MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS). Placeholders are inserted only as variable nodes (data-variable), never as {{name}} text. ContentPreview does not substitute example values for \"document\" content, because substituting a button or link href would make the schema drop that node. New exports from template-resolver/web: DocumentContentEditor, DocumentEditorProps, DocumentToolbarBlock, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.363.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Preview uses the examples of a stored or draft variable schema",
|
|
18
|
+
"detail": "A template's variableSchema kept only type and required, label and example were dropped, so the preview showed [name] instead of the declared example. The schema now keeps label and example, for mail and document previews."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.363.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "PDF meta gets orientation, printBackground and layoutHeaderFooter; paragraphs and headings get data-align",
|
|
24
|
+
"detail": "The document-pdf meta accepts orientation (portrait or landscape), printBackground (default true) and layoutHeaderFooter (false prints without the layout header and footer). Paragraphs and headings carry textAlign (left, center, right, justify), serialized as data-align and kept by the sanitizer only with these four values. New exports are evaluateDocumentDraft, localeSchema, DOCUMENT_TEXT_ALIGNMENTS and DocumentTextAlign; compileDocumentBodyToHandlebars accepts alignmentAsInlineStyle."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.363.0",
|
|
28
|
+
"type": "breaking",
|
|
29
|
+
"title": "document-pdf templates require contentFormat html, and html bodies are sanitized on write",
|
|
30
|
+
"detail": "upsertSystem, upsertTenant, the collection-set handlers and seedSystemTemplate rebuild html bodies of kind mail-html and document-pdf through the new document schema (template-resolver/document-schema). Script elements, inline event handlers, embedded content (iframe, frame, object, embed, applet, portal), links and url attributes (svg href, xlink:href, form action, base) outside https/http/mailto/tel/{{placeholder}}, external image sources, alt text containing < or >, and invalid or prototype-chain variable names are rejected with a validation_error (or a thrown Error in seedSystemTemplate); anything the schema cannot express is dropped silently, and links keep only href (no target, rel, class or title). New exports: documentExtensions, DocumentLink and the other Document* nodes, isAllowedDocumentLinkHref, isDocumentReferenceName, isDocumentResourceKey, isDocumentPlaceholderHref, sanitizeDocumentHtml, sanitizeTemplateBody. kind document-pdf with another contentFormat is rejected with document_requires_html_format.",
|
|
31
|
+
"migration": "Store document-pdf bodies as html and reference images via data-resource instead of src. mail-html with markdown or mjml is unchanged."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "0.363.0",
|
|
35
|
+
"type": "improvement",
|
|
36
|
+
"title": "Mail templates carry file attachments (PDF, PNG, JPEG)",
|
|
37
|
+
"detail": "linkedResources stays image only; file attachments of a mail template are listed in the optional TemplateResource.fileAttachments (kind file, fileRefId, fileName), sorted by key. template-resolver:write:resource-upload takes purpose \"attachment\" with a fileName; it checks the file by content (PDF, PNG, JPEG), keeps the original bytes, allows at most 10 per mail template and stores the entry next to the inline images. resource-remove, copy-locale and the bundle export/import carry attachments (bundle resource entries get purpose \"attachment\"). The preview lists attachments with kind \"inline\" or \"file\" (PreviewAttachment is now a union), send-test sends them, and the mail renderer attaches them after the inline images within the delivery attachment limit."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"version": "0.363.0",
|
|
41
|
+
"type": "improvement",
|
|
42
|
+
"title": "copy-locale can copy system templates (scope \"system\", system admins only)",
|
|
43
|
+
"detail": "template-resolver:write:copy-locale takes scope \"tenant\" (default) or \"system\". With \"system\" a system admin copies a system template to another locale as a draft; other callers get access denied."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"version": "0.363.0",
|
|
47
|
+
"type": "improvement",
|
|
48
|
+
"title": "Mail preview returns displayHtml for the editor",
|
|
49
|
+
"detail": "The preview of a mail template carries displayHtml, the body with cid images replaced by data URIs (png, jpeg, gif, webp, 5 MiB budget in total) so an editor can show it without cid support. The sent mail is unchanged."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"version": "0.363.0",
|
|
53
|
+
"type": "improvement",
|
|
54
|
+
"title": "Document editor gets variable labels, a handle, resource previews, canvas chrome, underline and PlaceholderLineEditor",
|
|
55
|
+
"detail": "DocumentContentEditor accepts variableLabels, systemVariables, resourcePreviews, chrome (\"default\" or \"canvas\") and onEditorReady (handle with insertPlaceholder, insertResource, focus). The new PlaceholderLineEditor edits plain text with {{name}} placeholders as chips on one line. Underline (u) is part of the document schema and survives the sanitizer."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"version": "0.363.0",
|
|
59
|
+
"type": "improvement",
|
|
60
|
+
"title": "Mail templates link a PDF rendered from a document template (resource-link-pdf)",
|
|
61
|
+
"detail": "New write template-resolver:write:resource-link-pdf takes templateId, key, templateSlug and fileName and stores a linkedResources entry { kind \"rendered-pdf\", templateSlug, fileName } in a mail template; nothing is uploaded. fileName is a one-line pattern that may use the mail's {{placeholders}} and must end with .pdf (no / \\ \" ; or ..). The document-pdf template must exist for the tenant or the system, otherwise the write fails with template_attachment_pdf_template_missing; an invalid pattern or slug fails with template_attachment_invalid. Uploaded files and linked PDFs share the limit of 10 attachments per template. resource-remove, copy-locale and the bundle export/import carry the entry (the bundle gets an optional pdfAttachments map of key to { templateSlug, fileName }; import does not check that the document template exists). TemplateResource gains pdfAttachmentLinks, LinkedResourceEntry gains the TemplatePdfAttachmentLink variant."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"version": "0.363.0",
|
|
65
|
+
"type": "improvement",
|
|
66
|
+
"title": "Preview and test mail use the images and attachments of the edited row, also while it is a draft",
|
|
67
|
+
"detail": "When preview or send-test get content and the caller has a row for (slug, kind, locale), that row supplies linkedResources whatever its status (also when it has none, so nothing is borrowed from the system template), so a tenant whose customization is saved as a draft sees its logo and attachments. MailDraft has an optional linkedResources; without it the stored active template's resources are used as before. PreviewAttachment gains the variant { kind \"rendered-pdf\", name, templateSlug } without sizeBytes, because the PDF is not rendered in the preview; size_under_cap does not count it."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"version": "0.363.0",
|
|
71
|
+
"type": "improvement",
|
|
72
|
+
"title": "Preview query and send-test write for mail and PDF templates",
|
|
73
|
+
"detail": "template-resolver:query:preview renders a draft, or the stored template when no content is given, with sample values from the declared variable examples (list variables get two rows). The draft passes the same write gates as a save and a mail goes through the real mail renderer, so subject, html, plain text and inline images match a real send. The response carries checks (images resolved, plain text present, size under 10 MiB, required placeholders used); a PDF template returns its sanitized body and the layout regions. template-resolver:write:send-test mails the rendered mail to the calling user's own address only, subject prefixed with [TEST], limited to 10 per user and hour. It needs delivery, channel-email and renderer-mail-html."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"version": "0.363.0",
|
|
77
|
+
"type": "improvement",
|
|
78
|
+
"title": "DocumentEditorProps toolbar option for an icon-only toolbar",
|
|
79
|
+
"detail": "DocumentEditorProps gains toolbar (\"full\" or \"compact\", default \"full\"). Compact renders the placeholder and block buttons icon-only with a tooltip and a narrower paragraph select, for narrow columns such as the layout editor regions. The formatting buttons now carry a tooltip with their label in every mode."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"version": "0.363.0",
|
|
83
|
+
"type": "breaking",
|
|
84
|
+
"title": "kind mail-html is replaced by mail, mail-layout and pdf-layout; templates carry meta",
|
|
85
|
+
"detail": "The kind mail-html is removed; the new non-render kind snippet holds collection content (reply snippets, signatures; any contentFormat, html is sanitized like a mail body). Mail bodies are kind mail (contentFormat html only, rebuilt through the document schema, meta.subject required), mail frames are kind mail-layout (contentFormat mjml), and pdf-layout (html) is new. The table gains a nullable meta column (JSON per kind, validated by parseTemplateMeta: mail takes subject, previewText, layoutSlug, category, sender; layouts take none; other kinds take no meta) and the template-resource entity moves to eventVersion 2 with an upcaster that reads stored mail-html events as mail. upsertSystem, upsertTenant (active), publish and seedSystemTemplate reject unknown placeholders and, for the same writes, bodies that miss a required variable of the notification with the same qualified name; a tenant draft may be incomplete and returns validation. Slugs must be kebab segments joined by a colon. seedSystemTemplate now validates the slug (templateSlugSchema) and the per-kind meta for every kind. A content collection of kind mail, mail-layout or pdf-layout is refused on collection-set (field issue on kind, templateResolver.errors.collection.kindNotAllowed), because such a tenant row would shadow the system template without the meta and placeholder gates. TemplateResource gains meta. New exports: parseTemplateMeta, isMailTemplateMeta, templateSlugSchema, validateTemplatePlaceholders, resolveTemplateVariables, compileDocumentBodyToHandlebars, MAIL_CATEGORIES and their types; seedNotificationTemplates and hasNotificationTemplates on template-resolver/seeding, which applyBootSeeds now calls. RENDER_KINDS contains mail instead of mail-html.",
|
|
86
|
+
"migration": "Rename kind mail-html to mail in app code, collections and seeds; mail bodies must be contentFormat html (markdown or mjml mail bodies are rejected) and carry meta.subject, mail layouts use kind mail-layout with contentFormat mjml and slug default. Slugs of every kind must now be kebab segments joined by a colon (upsert handlers and seedSystemTemplate reject dotted, underscored or camelCase slugs). Collections that used kind mail-html move to the new kind snippet (stored mail-html rows are upcast to mail, which collections refuse, so re-create those entries under snippet); collection-set refuses mail, mail-layout and pdf-layout collections. seedSystemTemplate callers must pass a kebab slug and valid meta for every kind, not only mail. Generate and apply the migration that adds the nullable meta column to read_template_resources; it ships a rebuild marker for read_template_resources so existing mail-html rows are read as mail after the rebuild."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"version": "0.363.0",
|
|
90
|
+
"type": "improvement",
|
|
91
|
+
"title": "resolveLayout finds a layout in any locale",
|
|
92
|
+
"detail": "TemplateResolverApi gains resolveLayout({ tenantId, slug, kind, locale }) for mail-layout and pdf-layout. It looks at the tenant first (requested locale, then the fallback locale, then any active locale) and then at the system layouts the same way, ignoring draft and archived rows. A layout saved by the layout editor under one locale is therefore found for every locale. The template preview uses it for the PDF layout."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"version": "0.363.0",
|
|
96
|
+
"type": "improvement",
|
|
97
|
+
"title": "PDF template meta, render provenance, system template lifecycle and override queries",
|
|
98
|
+
"detail": "kind document-pdf takes a strict meta (layoutSlug, pageFormat A4/Letter, margins in mm, fileName without / \\ \" ; or .., metadata title/author); new exports PDF_PAGE_FORMATS, PdfPageFormat, PdfTemplateMeta, readPdfTemplateMeta, DEFAULT_MAIL_LAYOUT_SLUG. RenderResponse (renderer-foundation) gains optional template and layout refs ({ templateId, templateVersion, scope }, new types RenderedTemplateRef and RenderProvenance); renderer-mail-html fills them. New handlers template-resolver:write:publish-system and archive-system (SystemAdmin), reset-to-system (TenantAdmin, SystemAdmin), queries usage, tenant-overrides (SystemAdmin, counts only) and diff-to-system. publish and archive answer not_found for rows outside the caller's tenant instead of a version_conflict; find-by-id also returns meta."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"version": "0.363.0",
|
|
102
|
+
"type": "breaking",
|
|
103
|
+
"title": "linkedResources is no longer a payload field of upsertSystem/upsertTenant; resourceUpload and resourceRemove manage it",
|
|
104
|
+
"detail": "Two steps - upload the image with POST /api/files (multipart, no entityType), then call template-resolver:write:resource-upload with { sourceFileRefId, templateId?, key? }. The handler re-encodes the caller's own unclaimed upload into a new file ref (width at most 1200 px) and links it into the template; resource-remove unlinks a key. upsertSystem and upsertTenant no longer accept linkedResources and leave the stored value untouched on update. The size cap is the system config key resource-max-bytes (default 2 MiB); a cap above the files route's maxUploadSize has no effect because the upload is rejected first. The source upload stays as an unclaimed file.",
|
|
105
|
+
"migration": "Whoever sent linkedResources with upsertSystem or upsertTenant now uploads the image via POST /api/files (multipart, no entityType) and links it with template-resolver:write:resource-upload { sourceFileRefId, templateId, key }. Server-side seeding (seedSystemTemplate linkedResources) stays."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"version": "0.363.0",
|
|
109
|
+
"type": "improvement",
|
|
110
|
+
"title": "Bundle import re-encodes its images like resource-upload; re-seeding keeps uploaded system resources",
|
|
111
|
+
"detail": "importBundle and importBundleSystem store each bundle resource through the same normalization as resource-upload (decoded format decides the content type, SVG and non-images are rejected, width at most 1200 px, resource-max-bytes applies), so an exported bundle carries the re-encoded bytes. seedSystemTemplate without linkedResources no longer resets the links of an existing row, so images a system admin uploaded survive the next boot. storeTemplateResource from the template-resolver barrel is the normalizing store; the raw byte variant is gone."
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"version": "0.363.0",
|
|
115
|
+
"type": "breaking",
|
|
116
|
+
"title": "Tenant mail-layouts are structured html, pdf-layout and document-pdf are placeholder-validated",
|
|
117
|
+
"detail": "A new validate query dry-runs the write pipeline (used, unknown and missing placeholders, canSave, canPublish). document-pdf, pdf-layout and mail-layout now go through the same placeholder gate as mail; document-pdf and pdf-layout know the system placeholders page and pages. A tenant mail-layout is html with a data-content slot plus header, signature and footer regions that renderer-mail-html wraps in a fixed MJML frame; raw mjml (with {{{content}}} exactly once) is system-only. New handlers export-bundle, import-bundle, import-bundle-system and copy-locale move a template with its resource files between tenants and locales. r.notification accepts a record mapping from an entity row to the declared variables, validated at boot. publish and archive no longer touch system rows through a tenant-scoped SystemAdmin.",
|
|
118
|
+
"migration": "Tenant mail-layout rows stored as mjml are ignored by the renderer, which falls back to the system layout; re-create them as html with a data-content slot. upsert-system answers with an extra validation field. seedSystemTemplate errors now read `<label> rejected: <code> (<detail>); ...`. Layout bodies without the content slot are rejected on write. `set` now runs the same gate for mail, document-pdf, pdf-layout and mail-layout rows and rejects unsanitized markup or publish issues, since saving publishes; the mail renderer also re-sanitizes stored tenant html layouts and falls back to the system layout when they fail. upsert-system and upsert-tenant no longer accept linkedResources (resources go through the resource upload; bundle import and copy-locale attach them). Unknown data-resource keys are a validate warning (resource_not_uploaded), not a write error."
|
|
119
|
+
},
|
|
2
120
|
{
|
|
3
121
|
"version": "0.354.0",
|
|
4
122
|
"type": "fix",
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { TemplateVariableDef, TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
|
+
import type { Element as ScanElement } from "happy-dom";
|
|
4
|
+
import { withParsedDocument } from "./document-sanitizer.js";
|
|
5
|
+
import {
|
|
6
|
+
DOCUMENT_TEXT_ALIGNMENTS,
|
|
7
|
+
isAllowedDocumentLinkHref,
|
|
8
|
+
isDocumentPlaceholderHref,
|
|
9
|
+
isDocumentReferenceName,
|
|
10
|
+
isDocumentResourceKey,
|
|
11
|
+
} from "./document-schema.js";
|
|
12
|
+
|
|
13
|
+
// Private-use characters mark where a Handlebars expression goes. Author text
|
|
14
|
+
// is scrubbed of them first and every `{`/`}` is entity-encoded before the
|
|
15
|
+
// markers are swapped for real expressions, so the only Handlebars syntax in
|
|
16
|
+
// the output is the syntax this module wrote.
|
|
17
|
+
const EXPRESSION_OPEN = "\uE000";
|
|
18
|
+
const EXPRESSION_CLOSE = "\uE001";
|
|
19
|
+
const MARKER_PATTERN = /\uE000(\d+)\uE001/g;
|
|
20
|
+
|
|
21
|
+
type RowScope = { readonly listName: string; readonly itemFields: TemplateVariables } | null;
|
|
22
|
+
|
|
23
|
+
type CompileState = {
|
|
24
|
+
readonly variables: TemplateVariables;
|
|
25
|
+
readonly expressions: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function marker(state: CompileState, expression: string): string {
|
|
29
|
+
state.expressions.push(expression);
|
|
30
|
+
return `${EXPRESSION_OPEN}${state.expressions.length - 1}${EXPRESSION_CLOSE}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ownVariable(variables: TemplateVariables, name: string): TemplateVariableDef | undefined {
|
|
34
|
+
return Object.hasOwn(variables, name) ? variables[name] : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function itemFieldsOf(variables: TemplateVariables, listName: string): TemplateVariables | null {
|
|
38
|
+
const def = ownVariable(variables, listName);
|
|
39
|
+
return def?.type === "list" ? def.item : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rowScopeOf(element: ScanElement, state: CompileState): RowScope {
|
|
43
|
+
const listName = element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
|
|
44
|
+
if (listName === null) return null;
|
|
45
|
+
const itemFields = itemFieldsOf(state.variables, listName);
|
|
46
|
+
return itemFields === null ? null : { listName, itemFields };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveReference(
|
|
50
|
+
name: string,
|
|
51
|
+
scope: RowScope,
|
|
52
|
+
variables: TemplateVariables,
|
|
53
|
+
): { readonly path: string; readonly def: TemplateVariableDef | undefined } {
|
|
54
|
+
if (scope !== null && ownVariable(scope.itemFields, name) !== undefined) {
|
|
55
|
+
return { path: name, def: ownVariable(scope.itemFields, name) };
|
|
56
|
+
}
|
|
57
|
+
return { path: scope === null ? name : `@root.${name}`, def: ownVariable(variables, name) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function valueExpression(path: string, def: TemplateVariableDef | undefined): string {
|
|
61
|
+
if (def?.type === "money") return `{{money ${path}}}`;
|
|
62
|
+
if (def?.type === "date") return `{{date ${path}}}`;
|
|
63
|
+
return `{{${path}}}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function rewriteVariable(element: ScanElement, state: CompileState): void {
|
|
67
|
+
const name = element.getAttribute("data-variable") ?? "";
|
|
68
|
+
const replacement = element.ownerDocument.createTextNode(
|
|
69
|
+
isDocumentReferenceName(name)
|
|
70
|
+
? marker(state, valueExpression(...pathAndDef(name, element, state)))
|
|
71
|
+
: "",
|
|
72
|
+
);
|
|
73
|
+
element.replaceWith(replacement);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pathAndDef(
|
|
77
|
+
name: string,
|
|
78
|
+
element: ScanElement,
|
|
79
|
+
state: CompileState,
|
|
80
|
+
): [string, TemplateVariableDef | undefined] {
|
|
81
|
+
const { path, def } = resolveReference(name, rowScopeOf(element, state), state.variables);
|
|
82
|
+
return [path, def];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rewriteLink(element: ScanElement, state: CompileState): void {
|
|
86
|
+
const href = element.getAttribute("href") ?? "";
|
|
87
|
+
if (isDocumentPlaceholderHref(href)) {
|
|
88
|
+
const name = href.slice(2, -2);
|
|
89
|
+
if (isDocumentReferenceName(name)) {
|
|
90
|
+
const [path] = pathAndDef(name, element, state);
|
|
91
|
+
element.setAttribute("href", marker(state, `{{link ${path}}}`));
|
|
92
|
+
} else {
|
|
93
|
+
element.setAttribute("href", "#");
|
|
94
|
+
}
|
|
95
|
+
} else if (!isAllowedDocumentLinkHref(href)) {
|
|
96
|
+
element.setAttribute("href", "#");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function rewriteRepeatRow(row: ScanElement, state: CompileState): void {
|
|
101
|
+
const listName = row.getAttribute("data-each") ?? "";
|
|
102
|
+
const document = row.ownerDocument;
|
|
103
|
+
if (itemFieldsOf(state.variables, listName) === null || !isDocumentReferenceName(listName)) {
|
|
104
|
+
row.remove();
|
|
105
|
+
} else {
|
|
106
|
+
row.before(document.createTextNode(marker(state, `{{#each ${listName}}}`)));
|
|
107
|
+
row.after(document.createTextNode(marker(state, "{{/each}}")));
|
|
108
|
+
row.removeAttribute("data-each");
|
|
109
|
+
row.removeAttribute("data-repeat-header");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stripActiveContent(element: ScanElement): void {
|
|
114
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
115
|
+
if (/^on/i.test(attribute.name)) element.removeAttribute(attribute.name);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function neutralizeBraces(html: string): string {
|
|
120
|
+
return html.replace(/[{}]/g, (brace) => (brace === "{" ? "{" : "}"));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type CompileDocumentBodyOptions = {
|
|
124
|
+
// Returns the src for an `img[data-resource]` key; undefined drops the image.
|
|
125
|
+
readonly resourceSrc?: (key: string) => string | undefined;
|
|
126
|
+
// Mail clients ignore document CSS, so mail turns data-align into an inline style.
|
|
127
|
+
readonly alignmentAsInlineStyle?: boolean;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const RESOURCE_IMAGE_CONTENT_TYPES: ReadonlySet<string> = new Set([
|
|
131
|
+
"image/png",
|
|
132
|
+
"image/jpeg",
|
|
133
|
+
"image/gif",
|
|
134
|
+
"image/webp",
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
// Unique valid resource keys in document order.
|
|
138
|
+
export function collectDocumentResourceKeys(html: string): readonly string[] {
|
|
139
|
+
return withParsedDocument(html, (root) => {
|
|
140
|
+
const keys = new Set<string>();
|
|
141
|
+
for (const element of Array.from(root.querySelectorAll("img[data-resource]"))) {
|
|
142
|
+
const key = element.getAttribute("data-resource") ?? "";
|
|
143
|
+
if (isDocumentResourceKey(key)) keys.add(key);
|
|
144
|
+
}
|
|
145
|
+
return [...keys];
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function toResourceDataUri(contentType: string, bytes: Uint8Array): string {
|
|
150
|
+
if (!RESOURCE_IMAGE_CONTENT_TYPES.has(contentType)) {
|
|
151
|
+
throw new Error(`resource content type "${contentType}" cannot be embedded as an image`);
|
|
152
|
+
}
|
|
153
|
+
return `data:${contentType};base64,${Buffer.from(bytes).toString("base64")}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function rewriteAlignment(element: ScanElement): void {
|
|
157
|
+
const align = element.getAttribute("data-align");
|
|
158
|
+
element.removeAttribute("data-align");
|
|
159
|
+
if (isOneOf(DOCUMENT_TEXT_ALIGNMENTS, align))
|
|
160
|
+
element.setAttribute("style", `text-align:${align}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function rewriteResourceImage(
|
|
164
|
+
element: ScanElement,
|
|
165
|
+
resourceSrc: CompileDocumentBodyOptions["resourceSrc"],
|
|
166
|
+
): void {
|
|
167
|
+
const key = element.getAttribute("data-resource") ?? "";
|
|
168
|
+
const src = isDocumentResourceKey(key) ? resourceSrc?.(key) : undefined;
|
|
169
|
+
if (src === undefined) {
|
|
170
|
+
element.remove();
|
|
171
|
+
// skip: an unresolved resource key drops the image
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
element.setAttribute("src", src);
|
|
175
|
+
element.removeAttribute("data-resource");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Turns a sanitized document body into Handlebars source. Rows and links are
|
|
179
|
+
// rewritten in document order; list rows last so their placeholders still see
|
|
180
|
+
// the `tr[data-each]` ancestor.
|
|
181
|
+
export function compileDocumentBodyToHandlebars(
|
|
182
|
+
html: string,
|
|
183
|
+
variables: TemplateVariables,
|
|
184
|
+
options: CompileDocumentBodyOptions = {},
|
|
185
|
+
): string {
|
|
186
|
+
const state: CompileState = { variables, expressions: [] };
|
|
187
|
+
const scrubbed = html.replaceAll(EXPRESSION_OPEN, "").replaceAll(EXPRESSION_CLOSE, "");
|
|
188
|
+
const compiled = withParsedDocument(scrubbed, (root) => {
|
|
189
|
+
for (const element of Array.from(root.querySelectorAll("script"))) element.remove();
|
|
190
|
+
for (const element of Array.from(root.querySelectorAll("img[data-resource]"))) {
|
|
191
|
+
rewriteResourceImage(element, options.resourceSrc);
|
|
192
|
+
}
|
|
193
|
+
for (const element of Array.from(root.querySelectorAll("*"))) stripActiveContent(element);
|
|
194
|
+
if (options.alignmentAsInlineStyle === true) {
|
|
195
|
+
for (const element of Array.from(root.querySelectorAll("[data-align]"))) {
|
|
196
|
+
rewriteAlignment(element);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const element of Array.from(root.querySelectorAll("span[data-variable]"))) {
|
|
200
|
+
rewriteVariable(element, state);
|
|
201
|
+
}
|
|
202
|
+
for (const element of Array.from(root.querySelectorAll("a[href]"))) {
|
|
203
|
+
rewriteLink(element, state);
|
|
204
|
+
}
|
|
205
|
+
for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
|
|
206
|
+
rewriteRepeatRow(element, state);
|
|
207
|
+
}
|
|
208
|
+
return root.innerHTML;
|
|
209
|
+
});
|
|
210
|
+
return neutralizeBraces(compiled).replace(
|
|
211
|
+
MARKER_PATTERN,
|
|
212
|
+
(_match, index: string) => state.expressions[Number(index)] ?? "",
|
|
213
|
+
);
|
|
214
|
+
}
|