@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
package/src/lib/upload.ts
CHANGED
|
@@ -5,40 +5,289 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { uuidv7 } from "uuidv7";
|
|
8
|
+
import type { MediaKind } from "../types/constants.js";
|
|
8
9
|
|
|
9
|
-
/** MIME types
|
|
10
|
-
const
|
|
10
|
+
/** MIME types — images */
|
|
11
|
+
const IMAGE_MIME_TYPES = [
|
|
11
12
|
"image/jpeg",
|
|
12
13
|
"image/png",
|
|
13
14
|
"image/gif",
|
|
14
15
|
"image/webp",
|
|
15
16
|
"image/svg+xml",
|
|
17
|
+
"image/avif",
|
|
18
|
+
"image/bmp",
|
|
19
|
+
"image/x-icon",
|
|
16
20
|
] as const;
|
|
17
21
|
|
|
18
|
-
/**
|
|
19
|
-
const
|
|
22
|
+
/** MIME types — video */
|
|
23
|
+
const VIDEO_MIME_TYPES = [
|
|
24
|
+
"video/mp4",
|
|
25
|
+
"video/webm",
|
|
26
|
+
"video/quicktime",
|
|
27
|
+
"video/x-msvideo",
|
|
28
|
+
"video/x-matroska",
|
|
29
|
+
"video/mpeg",
|
|
30
|
+
"video/3gpp",
|
|
31
|
+
"video/x-flv",
|
|
32
|
+
"video/ogg",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
/** MIME types — audio */
|
|
36
|
+
const AUDIO_MIME_TYPES = [
|
|
37
|
+
"audio/mpeg",
|
|
38
|
+
"audio/ogg",
|
|
39
|
+
"audio/wav",
|
|
40
|
+
"audio/mp4",
|
|
41
|
+
"audio/x-m4a",
|
|
42
|
+
"audio/flac",
|
|
43
|
+
"audio/aac",
|
|
44
|
+
"audio/webm",
|
|
45
|
+
"audio/x-aiff",
|
|
46
|
+
"audio/opus",
|
|
47
|
+
"audio/3gpp",
|
|
48
|
+
"audio/midi",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
/** MIME types — documents (books, PDFs) */
|
|
52
|
+
const DOCUMENT_MIME_TYPES = [
|
|
53
|
+
"application/pdf",
|
|
54
|
+
"application/epub+zip",
|
|
55
|
+
"application/x-mobipocket-ebook",
|
|
56
|
+
"application/vnd.amazon.ebook",
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
/** MIME types — office documents */
|
|
60
|
+
const OFFICE_MIME_TYPES = [
|
|
61
|
+
"application/msword",
|
|
62
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
63
|
+
"application/vnd.ms-excel",
|
|
64
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
65
|
+
"application/vnd.ms-powerpoint",
|
|
66
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
67
|
+
"application/vnd.oasis.opendocument.text",
|
|
68
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
69
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
70
|
+
"application/vnd.apple.pages",
|
|
71
|
+
"application/vnd.apple.numbers",
|
|
72
|
+
"application/vnd.apple.keynote",
|
|
73
|
+
] as const;
|
|
74
|
+
|
|
75
|
+
/** MIME types — text & structured data */
|
|
76
|
+
const TEXT_MIME_TYPES = [
|
|
77
|
+
"text/plain",
|
|
78
|
+
"text/markdown",
|
|
79
|
+
"text/csv",
|
|
80
|
+
"text/x-tiptap+json",
|
|
81
|
+
"text/html",
|
|
82
|
+
"text/css",
|
|
83
|
+
"text/javascript",
|
|
84
|
+
"text/xml",
|
|
85
|
+
"text/rtf",
|
|
86
|
+
"text/tab-separated-values",
|
|
87
|
+
"text/calendar",
|
|
88
|
+
"application/json",
|
|
89
|
+
"application/xml",
|
|
90
|
+
"application/yaml",
|
|
91
|
+
"application/toml",
|
|
92
|
+
] as const;
|
|
93
|
+
|
|
94
|
+
/** MIME types — archives */
|
|
95
|
+
const ARCHIVE_MIME_TYPES = [
|
|
96
|
+
"application/zip",
|
|
97
|
+
"application/x-tar",
|
|
98
|
+
"application/gzip",
|
|
99
|
+
"application/x-bzip2",
|
|
100
|
+
"application/x-7z-compressed",
|
|
101
|
+
"application/x-rar-compressed",
|
|
102
|
+
"application/zstd",
|
|
103
|
+
] as const;
|
|
104
|
+
|
|
105
|
+
/** MIME types — fonts */
|
|
106
|
+
const FONT_MIME_TYPES = [
|
|
107
|
+
"font/ttf",
|
|
108
|
+
"font/otf",
|
|
109
|
+
"font/woff",
|
|
110
|
+
"font/woff2",
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
/** MIME types — 3D & design */
|
|
114
|
+
const THREE_D_MIME_TYPES = [
|
|
115
|
+
"model/gltf+json",
|
|
116
|
+
"model/gltf-binary",
|
|
117
|
+
"model/obj",
|
|
118
|
+
"application/x-figma",
|
|
119
|
+
"image/vnd.dxf",
|
|
120
|
+
] as const;
|
|
121
|
+
|
|
122
|
+
/** MIME types — data & code */
|
|
123
|
+
const CODE_MIME_TYPES = [
|
|
124
|
+
"application/sql",
|
|
125
|
+
"application/wasm",
|
|
126
|
+
"application/x-ipynb+json",
|
|
127
|
+
"application/x-sh",
|
|
128
|
+
"application/x-python-code",
|
|
129
|
+
] as const;
|
|
130
|
+
|
|
131
|
+
/** Lookup table from MIME type to category */
|
|
132
|
+
const MIME_CATEGORY_MAP = new Map<string, MediaCategory>([
|
|
133
|
+
...IMAGE_MIME_TYPES.map((t) => [t, "image" as const] as const),
|
|
134
|
+
...VIDEO_MIME_TYPES.map((t) => [t, "video" as const] as const),
|
|
135
|
+
...AUDIO_MIME_TYPES.map((t) => [t, "audio" as const] as const),
|
|
136
|
+
...DOCUMENT_MIME_TYPES.map((t) => [t, "document" as const] as const),
|
|
137
|
+
...OFFICE_MIME_TYPES.map((t) => [t, "office" as const] as const),
|
|
138
|
+
...TEXT_MIME_TYPES.map((t) => [t, "text" as const] as const),
|
|
139
|
+
...ARCHIVE_MIME_TYPES.map((t) => [t, "archive" as const] as const),
|
|
140
|
+
...FONT_MIME_TYPES.map((t) => [t, "font" as const] as const),
|
|
141
|
+
...THREE_D_MIME_TYPES.map((t) => [t, "3d" as const] as const),
|
|
142
|
+
...CODE_MIME_TYPES.map((t) => [t, "code" as const] as const),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Accept string for file inputs. Accepts all file types.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* <input type="file" accept={UPLOAD_ACCEPT} />
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export const UPLOAD_ACCEPT = "*/*";
|
|
154
|
+
|
|
155
|
+
export type MediaCategory =
|
|
156
|
+
| "image"
|
|
157
|
+
| "video"
|
|
158
|
+
| "audio"
|
|
159
|
+
| "document"
|
|
160
|
+
| "office"
|
|
161
|
+
| "text"
|
|
162
|
+
| "archive"
|
|
163
|
+
| "font"
|
|
164
|
+
| "3d"
|
|
165
|
+
| "code";
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Returns the media category for a given MIME type.
|
|
169
|
+
* Unrecognized types default to "archive".
|
|
170
|
+
*
|
|
171
|
+
* @param mimeType - The MIME type to classify
|
|
172
|
+
* @returns The media category
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* getMediaCategory("video/mp4"); // "video"
|
|
176
|
+
* getMediaCategory("text/plain"); // "text"
|
|
177
|
+
* getMediaCategory("application/octet-stream"); // "archive"
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export function getMediaCategory(mimeType: string): MediaCategory {
|
|
181
|
+
// Exact match from known types
|
|
182
|
+
const exact = MIME_CATEGORY_MAP.get(mimeType);
|
|
183
|
+
if (exact) return exact;
|
|
184
|
+
|
|
185
|
+
// Prefix-based fallback for unknown subtypes
|
|
186
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
187
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
188
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
189
|
+
if (mimeType.startsWith("font/")) return "font";
|
|
190
|
+
if (mimeType.startsWith("model/")) return "3d";
|
|
191
|
+
if (mimeType.startsWith("text/")) return "text";
|
|
192
|
+
|
|
193
|
+
// Unknown types default to archive
|
|
194
|
+
return "archive";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Maps a MIME type to one of the five media kind categories.
|
|
199
|
+
* image/video/audio/text pass through; everything else becomes "document".
|
|
200
|
+
*
|
|
201
|
+
* @param mimeType - The MIME type to classify
|
|
202
|
+
* @returns The media kind
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* toMediaKind("image/jpeg"); // "image"
|
|
206
|
+
* toMediaKind("application/pdf"); // "document"
|
|
207
|
+
* toMediaKind("text/plain"); // "text"
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export function toMediaKind(mimeType: string): MediaKind {
|
|
211
|
+
const category = getMediaCategory(mimeType);
|
|
212
|
+
switch (category) {
|
|
213
|
+
case "image":
|
|
214
|
+
case "video":
|
|
215
|
+
case "audio":
|
|
216
|
+
case "text":
|
|
217
|
+
return category;
|
|
218
|
+
default:
|
|
219
|
+
return "document";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns true if the given MIME type is an image type.
|
|
225
|
+
*
|
|
226
|
+
* @param mimeType - The MIME type to check
|
|
227
|
+
* @returns Whether the MIME type is an image
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* isImageMimeType("image/jpeg"); // true
|
|
231
|
+
* isImageMimeType("video/mp4"); // false
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export function isImageMimeType(mimeType: string): boolean {
|
|
235
|
+
return mimeType.startsWith("image/");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface ValidateUploadOptions {
|
|
239
|
+
/** When true, only image MIME types are accepted (e.g. for avatar uploads). */
|
|
240
|
+
imagesOnly?: boolean;
|
|
241
|
+
/** Max file size in MB. */
|
|
242
|
+
maxFileSizeMB: number;
|
|
243
|
+
}
|
|
20
244
|
|
|
21
245
|
/**
|
|
22
246
|
* Validates an uploaded file's type and size.
|
|
23
247
|
*
|
|
24
248
|
* @param file - The uploaded File object
|
|
249
|
+
* @param options - Validation constraints
|
|
25
250
|
* @returns null if valid, error message string if invalid
|
|
26
251
|
* @example
|
|
27
252
|
* ```ts
|
|
28
|
-
* const error = validateUploadFile(file);
|
|
253
|
+
* const error = validateUploadFile(file, { maxFileSizeMB: 500 });
|
|
29
254
|
* if (error) return dsToast(error, "error");
|
|
30
255
|
* ```
|
|
31
256
|
*/
|
|
32
|
-
export function validateUploadFile(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
257
|
+
export function validateUploadFile(
|
|
258
|
+
file: File,
|
|
259
|
+
options: ValidateUploadOptions,
|
|
260
|
+
): string | null {
|
|
261
|
+
return validateUploadFileMetadata(file.type, file.size, options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Validates file metadata (type and size) without requiring a File object.
|
|
266
|
+
* Used by the multipart upload initiation endpoint which receives JSON metadata.
|
|
267
|
+
* All MIME types are accepted; unrecognized types are categorized as archive.
|
|
268
|
+
*
|
|
269
|
+
* @param contentType - The MIME type of the file
|
|
270
|
+
* @param size - The file size in bytes
|
|
271
|
+
* @param options - Validation constraints
|
|
272
|
+
* @returns null if valid, error message string if invalid
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* const error = validateUploadFileMetadata("image/jpeg", 1024000, { maxFileSizeMB: 500 });
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
export function validateUploadFileMetadata(
|
|
279
|
+
contentType: string,
|
|
280
|
+
size: number,
|
|
281
|
+
options: ValidateUploadOptions,
|
|
282
|
+
): string | null {
|
|
283
|
+
if (options?.imagesOnly) {
|
|
284
|
+
if (!isImageMimeType(contentType)) {
|
|
285
|
+
return "File type not allowed.";
|
|
286
|
+
}
|
|
39
287
|
}
|
|
40
|
-
|
|
41
|
-
|
|
288
|
+
const maxMB = options.maxFileSizeMB;
|
|
289
|
+
if (size > maxMB * 1024 * 1024) {
|
|
290
|
+
return `File too large (max ${maxMB}MB).`;
|
|
42
291
|
}
|
|
43
292
|
return null;
|
|
44
293
|
}
|
package/src/lib/url.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* URL Utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import limax from "limax";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Extracts the hostname (domain) from a URL string.
|
|
@@ -79,37 +79,52 @@ export function isFullUrl(str: string): boolean {
|
|
|
79
79
|
/**
|
|
80
80
|
* Converts text to a URL-friendly slug.
|
|
81
81
|
*
|
|
82
|
-
* Transforms text into a lowercase, hyphen-separated slug
|
|
83
|
-
* -
|
|
84
|
-
* - Removing special characters (keeping only word characters, spaces, and hyphens)
|
|
85
|
-
* - Replacing whitespace and underscores with hyphens
|
|
86
|
-
* - Removing leading and trailing hyphens
|
|
87
|
-
*
|
|
88
|
-
* Used for generating clean URLs from titles and names.
|
|
82
|
+
* Transforms text into a lowercase, hyphen-separated slug using limax for
|
|
83
|
+
* i18n-aware transliteration (CJK → Pinyin, Japanese → Romaji, accented → ASCII).
|
|
89
84
|
*
|
|
90
85
|
* @param text - The text to convert to a slug
|
|
91
86
|
* @returns The slugified string
|
|
92
87
|
*
|
|
93
88
|
* @example
|
|
94
89
|
* ```ts
|
|
95
|
-
*
|
|
90
|
+
* slugify("Hello World! This is a Test.");
|
|
96
91
|
* // Returns: "hello-world-this-is-a-test"
|
|
97
92
|
*
|
|
98
|
-
*
|
|
99
|
-
* // Returns: "
|
|
93
|
+
* slugify("书评");
|
|
94
|
+
* // Returns: "shu-ping"
|
|
100
95
|
* ```
|
|
101
96
|
*/
|
|
102
97
|
export function slugify(text: string): string {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
/[\u4e00-\u9fff\u3400-\u4dbf]+/g,
|
|
106
|
-
(match) => ` ${pinyin(match, { toneType: "none", separator: " " })} `,
|
|
107
|
-
);
|
|
98
|
+
return limax(text, { tone: false }).replace(/_/g, "-");
|
|
99
|
+
}
|
|
108
100
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
const SAFE_URL_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sanitizes a URL by ensuring it uses a safe protocol.
|
|
105
|
+
*
|
|
106
|
+
* Returns the URL unchanged if it uses an allowed protocol (http:, https:, mailto:)
|
|
107
|
+
* or is a relative path. Returns an empty string for dangerous protocols like
|
|
108
|
+
* `javascript:`, `data:`, or `vbscript:`.
|
|
109
|
+
*
|
|
110
|
+
* @param url - The URL string to sanitize
|
|
111
|
+
* @returns The original URL if safe, or an empty string if the protocol is disallowed
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* sanitizeUrl("https://example.com"); // "https://example.com"
|
|
116
|
+
* sanitizeUrl("/about"); // "/about"
|
|
117
|
+
* sanitizeUrl("javascript:alert(1)"); // ""
|
|
118
|
+
* sanitizeUrl("data:text/html,<h1>Hi</h1>"); // ""
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function sanitizeUrl(url: string): string {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = new URL(url, "https://placeholder.invalid");
|
|
124
|
+
// Relative URLs resolve against the placeholder and get https: — allow them
|
|
125
|
+
if (SAFE_URL_PROTOCOLS.has(parsed.protocol)) return url;
|
|
126
|
+
return "";
|
|
127
|
+
} catch {
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
115
130
|
}
|