@jant/core 0.3.35 → 0.3.37
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/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
RedirectTypeSchema,
|
|
6
6
|
CreatePostSchema,
|
|
7
7
|
UpdatePostSchema,
|
|
8
|
+
SetupSchema,
|
|
9
|
+
SigninSchema,
|
|
10
|
+
normalizeEmail,
|
|
8
11
|
parseFormData,
|
|
9
12
|
parseFormDataOptional,
|
|
10
13
|
validateMediaCount,
|
|
@@ -52,6 +55,46 @@ describe("RedirectTypeSchema", () => {
|
|
|
52
55
|
});
|
|
53
56
|
});
|
|
54
57
|
|
|
58
|
+
describe("normalizeEmail", () => {
|
|
59
|
+
it("trims and lowercases email addresses", () => {
|
|
60
|
+
expect(normalizeEmail(" User.Name+tag@Example.COM ")).toBe(
|
|
61
|
+
"user.name+tag@example.com",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("SetupSchema", () => {
|
|
67
|
+
it("normalizes email before returning parsed data", () => {
|
|
68
|
+
const result = SetupSchema.parse({
|
|
69
|
+
siteName: "Jant",
|
|
70
|
+
email: " Admin@Example.COM ",
|
|
71
|
+
password: "password123",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(result.email).toBe("admin@example.com");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("SigninSchema", () => {
|
|
79
|
+
it("normalizes email before returning parsed data", () => {
|
|
80
|
+
const result = SigninSchema.parse({
|
|
81
|
+
email: " Admin@Example.COM ",
|
|
82
|
+
password: "password123",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result.email).toBe("admin@example.com");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("rejects invalid email after normalization", () => {
|
|
89
|
+
expect(() =>
|
|
90
|
+
SigninSchema.parse({
|
|
91
|
+
email: " not-an-email ",
|
|
92
|
+
password: "password123",
|
|
93
|
+
}),
|
|
94
|
+
).toThrow();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
55
98
|
describe("CreatePostSchema", () => {
|
|
56
99
|
const validPost = {
|
|
57
100
|
format: "note",
|
|
@@ -67,11 +110,23 @@ describe("CreatePostSchema", () => {
|
|
|
67
110
|
});
|
|
68
111
|
|
|
69
112
|
it("accepts all formats", () => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
expect(() =>
|
|
114
|
+
CreatePostSchema.parse({ ...validPost, format: "note" }),
|
|
115
|
+
).not.toThrow();
|
|
116
|
+
expect(() =>
|
|
117
|
+
CreatePostSchema.parse({
|
|
118
|
+
...validPost,
|
|
119
|
+
format: "link",
|
|
120
|
+
url: "https://example.com",
|
|
121
|
+
}),
|
|
122
|
+
).not.toThrow();
|
|
123
|
+
expect(() =>
|
|
124
|
+
CreatePostSchema.parse({
|
|
125
|
+
...validPost,
|
|
126
|
+
format: "quote",
|
|
127
|
+
quoteText: "A wise person once said...",
|
|
128
|
+
}),
|
|
129
|
+
).not.toThrow();
|
|
75
130
|
});
|
|
76
131
|
|
|
77
132
|
it("accepts optional title", () => {
|
|
@@ -82,88 +137,68 @@ describe("CreatePostSchema", () => {
|
|
|
82
137
|
expect(result.title).toBe("My Post");
|
|
83
138
|
});
|
|
84
139
|
|
|
85
|
-
it("accepts valid
|
|
140
|
+
it("accepts valid slug format", () => {
|
|
86
141
|
const result = CreatePostSchema.parse({
|
|
87
142
|
...validPost,
|
|
88
|
-
|
|
143
|
+
slug: "my-post-slug",
|
|
89
144
|
});
|
|
90
|
-
expect(result.
|
|
145
|
+
expect(result.slug).toBe("my-post-slug");
|
|
91
146
|
});
|
|
92
147
|
|
|
93
|
-
it("accepts single-character
|
|
148
|
+
it("accepts single-character slug", () => {
|
|
94
149
|
const result = CreatePostSchema.parse({
|
|
95
150
|
...validPost,
|
|
96
|
-
|
|
151
|
+
slug: "a",
|
|
97
152
|
});
|
|
98
|
-
expect(result.
|
|
153
|
+
expect(result.slug).toBe("a");
|
|
99
154
|
});
|
|
100
155
|
|
|
101
|
-
it("accepts empty
|
|
102
|
-
const result = CreatePostSchema.parse({ ...validPost,
|
|
103
|
-
expect(result.
|
|
156
|
+
it("accepts empty slug (transforms to undefined)", () => {
|
|
157
|
+
const result = CreatePostSchema.parse({ ...validPost, slug: "" });
|
|
158
|
+
expect(result.slug).toBeUndefined();
|
|
104
159
|
});
|
|
105
160
|
|
|
106
|
-
it("
|
|
161
|
+
it("normalizes uppercase slug", () => {
|
|
162
|
+
const result = CreatePostSchema.parse({ ...validPost, slug: "MyPost" });
|
|
163
|
+
expect(result.slug).toBe("mypost");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("normalizes special chars in slug", () => {
|
|
107
167
|
const result = CreatePostSchema.parse({
|
|
108
168
|
...validPost,
|
|
109
|
-
|
|
169
|
+
slug: "my post!",
|
|
110
170
|
});
|
|
111
|
-
expect(result.
|
|
171
|
+
expect(result.slug).toBe("my-post");
|
|
112
172
|
});
|
|
113
173
|
|
|
114
|
-
it("
|
|
174
|
+
it("normalizes leading hyphen in slug", () => {
|
|
115
175
|
const result = CreatePostSchema.parse({
|
|
116
176
|
...validPost,
|
|
117
|
-
|
|
177
|
+
slug: "-my-post",
|
|
118
178
|
});
|
|
119
|
-
expect(result.
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("rejects invalid path format (uppercase)", () => {
|
|
123
|
-
expect(() =>
|
|
124
|
-
CreatePostSchema.parse({ ...validPost, path: "MyPost" }),
|
|
125
|
-
).toThrow();
|
|
179
|
+
expect(result.slug).toBe("my-post");
|
|
126
180
|
});
|
|
127
181
|
|
|
128
|
-
it("
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
it("rejects path starting with hyphen", () => {
|
|
135
|
-
expect(() =>
|
|
136
|
-
CreatePostSchema.parse({ ...validPost, path: "-my-post" }),
|
|
137
|
-
).toThrow();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("rejects path ending with hyphen", () => {
|
|
141
|
-
expect(() =>
|
|
142
|
-
CreatePostSchema.parse({ ...validPost, path: "my-post-" }),
|
|
143
|
-
).toThrow();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("rejects path with leading slash", () => {
|
|
147
|
-
expect(() =>
|
|
148
|
-
CreatePostSchema.parse({ ...validPost, path: "/my-post" }),
|
|
149
|
-
).toThrow();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("rejects path with trailing slash", () => {
|
|
153
|
-
expect(() =>
|
|
154
|
-
CreatePostSchema.parse({ ...validPost, path: "my-post/" }),
|
|
155
|
-
).toThrow();
|
|
182
|
+
it("normalizes trailing hyphen in slug", () => {
|
|
183
|
+
const result = CreatePostSchema.parse({
|
|
184
|
+
...validPost,
|
|
185
|
+
slug: "my-post-",
|
|
186
|
+
});
|
|
187
|
+
expect(result.slug).toBe("my-post");
|
|
156
188
|
});
|
|
157
189
|
|
|
158
|
-
it("
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
190
|
+
it("normalizes slashes in slug", () => {
|
|
191
|
+
const result = CreatePostSchema.parse({
|
|
192
|
+
...validPost,
|
|
193
|
+
slug: "my/post",
|
|
194
|
+
});
|
|
195
|
+
expect(result.slug).toBe("my-post");
|
|
162
196
|
});
|
|
163
197
|
|
|
164
198
|
it("accepts valid url", () => {
|
|
165
199
|
const result = CreatePostSchema.parse({
|
|
166
200
|
...validPost,
|
|
201
|
+
format: "link",
|
|
167
202
|
url: "https://example.com",
|
|
168
203
|
});
|
|
169
204
|
expect(result.url).toBe("https://example.com");
|
|
@@ -231,14 +266,28 @@ describe("CreatePostSchema", () => {
|
|
|
231
266
|
).toThrow();
|
|
232
267
|
});
|
|
233
268
|
|
|
269
|
+
it("accepts visibility values", () => {
|
|
270
|
+
for (const v of ["public", "unlisted", "private"]) {
|
|
271
|
+
const result = CreatePostSchema.parse({ ...validPost, visibility: v });
|
|
272
|
+
expect(result.visibility).toBe(v);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
234
276
|
it("accepts featured as boolean", () => {
|
|
235
277
|
const result = CreatePostSchema.parse({ ...validPost, featured: true });
|
|
236
278
|
expect(result.featured).toBe(true);
|
|
237
279
|
});
|
|
238
280
|
|
|
239
|
-
it("
|
|
240
|
-
|
|
241
|
-
|
|
281
|
+
it("rejects featured as non-boolean (other than 'on')", () => {
|
|
282
|
+
expect(() =>
|
|
283
|
+
CreatePostSchema.parse({ ...validPost, featured: "invalid" }),
|
|
284
|
+
).toThrow();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("rejects invalid visibility", () => {
|
|
288
|
+
expect(() =>
|
|
289
|
+
CreatePostSchema.parse({ ...validPost, visibility: "hidden" }),
|
|
290
|
+
).toThrow();
|
|
242
291
|
});
|
|
243
292
|
|
|
244
293
|
it("accepts pinned as boolean", () => {
|
|
@@ -251,14 +300,62 @@ describe("CreatePostSchema", () => {
|
|
|
251
300
|
expect(result.pinned).toBe(true);
|
|
252
301
|
});
|
|
253
302
|
|
|
254
|
-
it("accepts optional quoteText", () => {
|
|
303
|
+
it("accepts optional quoteText for quote posts", () => {
|
|
255
304
|
const result = CreatePostSchema.parse({
|
|
256
305
|
...validPost,
|
|
306
|
+
format: "quote",
|
|
257
307
|
quoteText: "A wise person once said...",
|
|
258
308
|
});
|
|
259
309
|
expect(result.quoteText).toBe("A wise person once said...");
|
|
260
310
|
});
|
|
261
311
|
|
|
312
|
+
it("rejects note posts with a URL", () => {
|
|
313
|
+
expect(() =>
|
|
314
|
+
CreatePostSchema.parse({
|
|
315
|
+
...validPost,
|
|
316
|
+
url: "https://example.com",
|
|
317
|
+
}),
|
|
318
|
+
).toThrow("Notes can't include a URL.");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("rejects note posts with quoted text", () => {
|
|
322
|
+
expect(() =>
|
|
323
|
+
CreatePostSchema.parse({
|
|
324
|
+
...validPost,
|
|
325
|
+
quoteText: "A wise person once said...",
|
|
326
|
+
}),
|
|
327
|
+
).toThrow("Notes can't include quoted text.");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("rejects link posts without a URL", () => {
|
|
331
|
+
expect(() =>
|
|
332
|
+
CreatePostSchema.parse({
|
|
333
|
+
...validPost,
|
|
334
|
+
format: "link",
|
|
335
|
+
}),
|
|
336
|
+
).toThrow("Link posts need a URL.");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("rejects link posts with quoted text", () => {
|
|
340
|
+
expect(() =>
|
|
341
|
+
CreatePostSchema.parse({
|
|
342
|
+
...validPost,
|
|
343
|
+
format: "link",
|
|
344
|
+
url: "https://example.com",
|
|
345
|
+
quoteText: "A wise person once said...",
|
|
346
|
+
}),
|
|
347
|
+
).toThrow("Link posts can't include quoted text.");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("rejects quote posts without quoted text", () => {
|
|
351
|
+
expect(() =>
|
|
352
|
+
CreatePostSchema.parse({
|
|
353
|
+
...validPost,
|
|
354
|
+
format: "quote",
|
|
355
|
+
}),
|
|
356
|
+
).toThrow("Quote posts need quoted text.");
|
|
357
|
+
});
|
|
358
|
+
|
|
262
359
|
it("accepts optional rating (1-5)", () => {
|
|
263
360
|
for (const rating of [1, 2, 3, 4, 5]) {
|
|
264
361
|
const result = CreatePostSchema.parse({ ...validPost, rating });
|
|
@@ -283,12 +380,18 @@ describe("CreatePostSchema", () => {
|
|
|
283
380
|
expect(result.rating).toBeUndefined();
|
|
284
381
|
});
|
|
285
382
|
|
|
286
|
-
it("accepts optional collectionIds as array of
|
|
383
|
+
it("accepts optional collectionIds as array of non-empty strings", () => {
|
|
287
384
|
const result = CreatePostSchema.parse({
|
|
288
385
|
...validPost,
|
|
289
|
-
collectionIds: [1, 2, 3],
|
|
386
|
+
collectionIds: ["col-1", "col-2", "col-3"],
|
|
290
387
|
});
|
|
291
|
-
expect(result.collectionIds).toEqual([1, 2, 3]);
|
|
388
|
+
expect(result.collectionIds).toEqual(["col-1", "col-2", "col-3"]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("rejects collectionIds with empty strings", () => {
|
|
392
|
+
expect(() =>
|
|
393
|
+
CreatePostSchema.parse({ ...validPost, collectionIds: [""] }),
|
|
394
|
+
).toThrow();
|
|
292
395
|
});
|
|
293
396
|
|
|
294
397
|
it("accepts empty string collectionIds (transforms to undefined)", () => {
|
|
@@ -316,6 +419,24 @@ describe("CreatePostSchema", () => {
|
|
|
316
419
|
expect(() => CreatePostSchema.parse({})).toThrow();
|
|
317
420
|
expect(() => CreatePostSchema.parse({ body: "hello" })).toThrow();
|
|
318
421
|
});
|
|
422
|
+
|
|
423
|
+
it("accepts bodyMarkdown", () => {
|
|
424
|
+
const result = CreatePostSchema.parse({
|
|
425
|
+
format: "note",
|
|
426
|
+
bodyMarkdown: "Hello **world**",
|
|
427
|
+
});
|
|
428
|
+
expect(result.bodyMarkdown).toBe("Hello **world**");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("rejects both body and bodyMarkdown", () => {
|
|
432
|
+
expect(() =>
|
|
433
|
+
CreatePostSchema.parse({
|
|
434
|
+
format: "note",
|
|
435
|
+
body: '{"type":"doc","content":[]}',
|
|
436
|
+
bodyMarkdown: "Hello",
|
|
437
|
+
}),
|
|
438
|
+
).toThrow("Provide either body or bodyMarkdown, not both");
|
|
439
|
+
});
|
|
319
440
|
});
|
|
320
441
|
|
|
321
442
|
describe("UpdatePostSchema", () => {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { generatePostSlug } from "../slug.js";
|
|
3
|
+
|
|
4
|
+
/** Helper: always-available check */
|
|
5
|
+
const alwaysAvailable = async () => true;
|
|
6
|
+
|
|
7
|
+
/** Helper: never-available check */
|
|
8
|
+
const neverAvailable = async () => false;
|
|
9
|
+
|
|
10
|
+
/** Helper: available after N calls */
|
|
11
|
+
function availableAfter(n: number) {
|
|
12
|
+
let calls = 0;
|
|
13
|
+
return async () => {
|
|
14
|
+
calls++;
|
|
15
|
+
return calls > n;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("generatePostSlug", () => {
|
|
20
|
+
describe("user-provided slug", () => {
|
|
21
|
+
it("returns user slug when available", async () => {
|
|
22
|
+
const slug = await generatePostSlug({
|
|
23
|
+
slug: "my-post",
|
|
24
|
+
idLength: 5,
|
|
25
|
+
isAvailable: alwaysAvailable,
|
|
26
|
+
});
|
|
27
|
+
expect(slug).toBe("my-post");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("throws ConflictError when slug is taken", async () => {
|
|
31
|
+
await expect(
|
|
32
|
+
generatePostSlug({
|
|
33
|
+
slug: "taken-slug",
|
|
34
|
+
idLength: 5,
|
|
35
|
+
isAvailable: neverAvailable,
|
|
36
|
+
}),
|
|
37
|
+
).rejects.toThrow("already in use");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("throws ValidationError for reserved slug", async () => {
|
|
41
|
+
await expect(
|
|
42
|
+
generatePostSlug({
|
|
43
|
+
slug: "dash",
|
|
44
|
+
idLength: 5,
|
|
45
|
+
isAvailable: alwaysAvailable,
|
|
46
|
+
}),
|
|
47
|
+
).rejects.toThrow("reserved");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("prioritizes user slug over title", async () => {
|
|
51
|
+
const slug = await generatePostSlug({
|
|
52
|
+
slug: "custom-slug",
|
|
53
|
+
title: "My Title",
|
|
54
|
+
idLength: 5,
|
|
55
|
+
isAvailable: alwaysAvailable,
|
|
56
|
+
});
|
|
57
|
+
expect(slug).toBe("custom-slug");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("title-based slug", () => {
|
|
62
|
+
it("generates slug from title", async () => {
|
|
63
|
+
const slug = await generatePostSlug({
|
|
64
|
+
title: "Hello World",
|
|
65
|
+
idLength: 5,
|
|
66
|
+
isAvailable: alwaysAvailable,
|
|
67
|
+
});
|
|
68
|
+
expect(slug).toBe("hello-world");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("appends random suffix on conflict", async () => {
|
|
72
|
+
const slug = await generatePostSlug({
|
|
73
|
+
title: "Hello World",
|
|
74
|
+
idLength: 5,
|
|
75
|
+
isAvailable: availableAfter(1), // first call (base) fails, second succeeds
|
|
76
|
+
});
|
|
77
|
+
expect(slug).toMatch(/^hello-world-[a-z0-9]{5}$/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("retries with different random suffixes", async () => {
|
|
81
|
+
const slug = await generatePostSlug({
|
|
82
|
+
title: "Test Post",
|
|
83
|
+
idLength: 5,
|
|
84
|
+
isAvailable: availableAfter(3),
|
|
85
|
+
});
|
|
86
|
+
expect(slug).toMatch(/^test-post-[a-z0-9]{5}$/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("throws after exceeding max retries", async () => {
|
|
90
|
+
await expect(
|
|
91
|
+
generatePostSlug({
|
|
92
|
+
title: "Test Post",
|
|
93
|
+
idLength: 5,
|
|
94
|
+
isAvailable: neverAvailable,
|
|
95
|
+
}),
|
|
96
|
+
).rejects.toThrow("Could not generate a unique slug");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("random slug (no title, no slug)", () => {
|
|
101
|
+
it("generates random ID of specified length", async () => {
|
|
102
|
+
const slug = await generatePostSlug({
|
|
103
|
+
idLength: 5,
|
|
104
|
+
isAvailable: alwaysAvailable,
|
|
105
|
+
});
|
|
106
|
+
expect(slug).toMatch(/^[a-z0-9]{5}$/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("retries on conflict", async () => {
|
|
110
|
+
const slug = await generatePostSlug({
|
|
111
|
+
idLength: 8,
|
|
112
|
+
isAvailable: availableAfter(2),
|
|
113
|
+
});
|
|
114
|
+
expect(slug).toMatch(/^[a-z0-9]{8}$/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("throws after exceeding max retries", async () => {
|
|
118
|
+
await expect(
|
|
119
|
+
generatePostSlug({
|
|
120
|
+
idLength: 5,
|
|
121
|
+
isAvailable: neverAvailable,
|
|
122
|
+
}),
|
|
123
|
+
).rejects.toThrow("Could not generate a unique slug");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -3,20 +3,20 @@ import { dsRedirect, dsToast, dsSignals } from "../sse.js";
|
|
|
3
3
|
|
|
4
4
|
describe("dsRedirect", () => {
|
|
5
5
|
it("returns a Response with text/html content-type", () => {
|
|
6
|
-
const res = dsRedirect("/
|
|
6
|
+
const res = dsRedirect("/settings");
|
|
7
7
|
expect(res.headers.get("Content-Type")).toBe("text/html");
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
it("includes Datastar headers for append mode", () => {
|
|
11
|
-
const res = dsRedirect("/
|
|
11
|
+
const res = dsRedirect("/settings");
|
|
12
12
|
expect(res.headers.get("Datastar-Mode")).toBe("append");
|
|
13
13
|
expect(res.headers.get("Datastar-Selector")).toBe("body");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it("body contains redirect script with correct URL", async () => {
|
|
17
|
-
const res = dsRedirect("/
|
|
17
|
+
const res = dsRedirect("/settings/general");
|
|
18
18
|
const body = await res.text();
|
|
19
|
-
expect(body).toContain("window.location.href='/
|
|
19
|
+
expect(body).toContain("window.location.href='/settings/general'");
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("escapes single quotes in URL", async () => {
|
|
@@ -26,7 +26,7 @@ describe("dsRedirect", () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it("merges additional headers from plain object", () => {
|
|
29
|
-
const res = dsRedirect("/
|
|
29
|
+
const res = dsRedirect("/settings", {
|
|
30
30
|
headers: { "Set-Cookie": "session=abc" },
|
|
31
31
|
});
|
|
32
32
|
expect(res.headers.get("Set-Cookie")).toBe("session=abc");
|
|
@@ -37,7 +37,7 @@ describe("dsRedirect", () => {
|
|
|
37
37
|
const headers = new Headers();
|
|
38
38
|
headers.append("set-cookie", "session=abc; Path=/; HttpOnly");
|
|
39
39
|
headers.append("set-cookie", "data=xyz; Path=/; Max-Age=300");
|
|
40
|
-
const res = dsRedirect("/
|
|
40
|
+
const res = dsRedirect("/settings", { headers });
|
|
41
41
|
const cookies = res.headers.getSetCookie();
|
|
42
42
|
expect(cookies).toHaveLength(2);
|
|
43
43
|
expect(cookies[0]).toBe("session=abc; Path=/; HttpOnly");
|