@jant/core 0.3.27 → 0.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +21 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +111 -174
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -267
- package/dist/auth.js +0 -39
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
package/dist/lib/favicon.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Favicon Utilities
|
|
3
|
-
*
|
|
4
|
-
* Sizes and ICO encoding for generated favicon variants.
|
|
5
|
-
* Favicon data is stored as base64 in the settings table (not R2)
|
|
6
|
-
* since the files are tiny and accessed on every page load.
|
|
7
|
-
*/ /**
|
|
8
|
-
* Favicon variant sizes (width x height in pixels)
|
|
9
|
-
*/ export const FAVICON_SIZES = {
|
|
10
|
-
ICO_16: 16,
|
|
11
|
-
ICO_32: 32,
|
|
12
|
-
APPLE_TOUCH: 180
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Encode PNG images into an ICO file.
|
|
16
|
-
*
|
|
17
|
-
* ICO format (with PNG payloads):
|
|
18
|
-
* - Header: 6 bytes (reserved=0, type=1, count=N)
|
|
19
|
-
* - Directory: 16 bytes per entry (width, height, colors, reserved, planes, bpp, size, offset)
|
|
20
|
-
* - Data: raw PNG bytes for each entry
|
|
21
|
-
*
|
|
22
|
-
* @param entries - Array of { size, png } where png is an ArrayBuffer of PNG data
|
|
23
|
-
* @returns ICO file as a Blob
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```ts
|
|
27
|
-
* const ico = encodeIco([
|
|
28
|
-
* { size: 16, png: png16ArrayBuffer },
|
|
29
|
-
* { size: 32, png: png32ArrayBuffer },
|
|
30
|
-
* ]);
|
|
31
|
-
* ```
|
|
32
|
-
*/ export function encodeIco(entries) {
|
|
33
|
-
const headerSize = 6;
|
|
34
|
-
const dirEntrySize = 16;
|
|
35
|
-
const dirSize = entries.length * dirEntrySize;
|
|
36
|
-
let dataOffset = headerSize + dirSize;
|
|
37
|
-
// Build header + directory
|
|
38
|
-
const header = new ArrayBuffer(headerSize + dirSize);
|
|
39
|
-
const view = new DataView(header);
|
|
40
|
-
// ICO header
|
|
41
|
-
view.setUint16(0, 0, true); // reserved
|
|
42
|
-
view.setUint16(2, 1, true); // type = icon
|
|
43
|
-
view.setUint16(4, entries.length, true); // count
|
|
44
|
-
const pngBuffers = [];
|
|
45
|
-
for(let i = 0; i < entries.length; i++){
|
|
46
|
-
const entry = entries[i];
|
|
47
|
-
const offset = headerSize + i * dirEntrySize;
|
|
48
|
-
// Width/height: 0 means 256
|
|
49
|
-
view.setUint8(offset + 0, entry.size < 256 ? entry.size : 0);
|
|
50
|
-
view.setUint8(offset + 1, entry.size < 256 ? entry.size : 0);
|
|
51
|
-
view.setUint8(offset + 2, 0); // color count (0 for >256 colors)
|
|
52
|
-
view.setUint8(offset + 3, 0); // reserved
|
|
53
|
-
view.setUint16(offset + 4, 1, true); // color planes
|
|
54
|
-
view.setUint16(offset + 6, 32, true); // bits per pixel
|
|
55
|
-
view.setUint32(offset + 8, entry.png.byteLength, true); // image size
|
|
56
|
-
view.setUint32(offset + 12, dataOffset, true); // image offset
|
|
57
|
-
dataOffset += entry.png.byteLength;
|
|
58
|
-
pngBuffers.push(entry.png);
|
|
59
|
-
}
|
|
60
|
-
return new Blob([
|
|
61
|
-
header,
|
|
62
|
-
...pngBuffers
|
|
63
|
-
], {
|
|
64
|
-
type: "image/x-icon"
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Convert an ArrayBuffer to a base64 string.
|
|
69
|
-
*
|
|
70
|
-
* @param buffer - The ArrayBuffer to encode
|
|
71
|
-
* @returns base64-encoded string
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```ts
|
|
75
|
-
* const b64 = arrayBufferToBase64(await blob.arrayBuffer());
|
|
76
|
-
* ```
|
|
77
|
-
*/ export function arrayBufferToBase64(buffer) {
|
|
78
|
-
const bytes = new Uint8Array(buffer);
|
|
79
|
-
let binary = "";
|
|
80
|
-
for(let i = 0; i < bytes.byteLength; i++){
|
|
81
|
-
binary += String.fromCharCode(bytes[i]);
|
|
82
|
-
}
|
|
83
|
-
return btoa(binary);
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Convert a base64 string to a Uint8Array.
|
|
87
|
-
*
|
|
88
|
-
* @param base64 - The base64 string to decode
|
|
89
|
-
* @returns decoded Uint8Array
|
|
90
|
-
*
|
|
91
|
-
* @example
|
|
92
|
-
* ```ts
|
|
93
|
-
* const bytes = base64ToUint8Array(storedBase64);
|
|
94
|
-
* ```
|
|
95
|
-
*/ export function base64ToUint8Array(base64) {
|
|
96
|
-
const binary = atob(base64);
|
|
97
|
-
const bytes = new Uint8Array(binary.length);
|
|
98
|
-
for(let i = 0; i < binary.length; i++){
|
|
99
|
-
bytes[i] = binary.charCodeAt(i);
|
|
100
|
-
}
|
|
101
|
-
return bytes;
|
|
102
|
-
}
|
package/dist/lib/feed.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default Feed Renderers
|
|
3
|
-
*
|
|
4
|
-
* RSS 2.0, Atom, and Sitemap XML generators.
|
|
5
|
-
* Theme authors can import these to extend/wrap the defaults:
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import { defaultRssRenderer } from "@jant/core/lib/feed";
|
|
10
|
-
* ```
|
|
11
|
-
*/ /**
|
|
12
|
-
* Escape special XML characters.
|
|
13
|
-
*/ function escapeXml(str) {
|
|
14
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Default RSS 2.0 renderer.
|
|
18
|
-
*
|
|
19
|
-
* @param data - Feed data with PostView[] (pre-computed URLs)
|
|
20
|
-
* @returns RSS 2.0 XML string
|
|
21
|
-
*/ export function defaultRssRenderer(data) {
|
|
22
|
-
const { siteName, siteDescription, siteUrl, siteLanguage, posts } = data;
|
|
23
|
-
const items = posts.map((post)=>{
|
|
24
|
-
const link = `${siteUrl}${post.permalink}`;
|
|
25
|
-
const title = post.title || `Post #${post.id}`;
|
|
26
|
-
const pubDate = new Date(post.publishedAt).toUTCString();
|
|
27
|
-
// Add enclosure for first media attachment
|
|
28
|
-
const firstMedia = post.media[0];
|
|
29
|
-
const enclosure = firstMedia ? `\n <enclosure url="${firstMedia.url}" type="${firstMedia.mimeType}"${firstMedia.size ? ` length="${firstMedia.size}"` : ""}/>` : "";
|
|
30
|
-
return `
|
|
31
|
-
<item>
|
|
32
|
-
<title><![CDATA[${escapeXml(title)}]]></title>
|
|
33
|
-
<link>${link}</link>
|
|
34
|
-
<guid isPermaLink="true">${link}</guid>
|
|
35
|
-
<pubDate>${pubDate}</pubDate>
|
|
36
|
-
<description><![CDATA[${post.bodyHtml || ""}]]></description>${enclosure}
|
|
37
|
-
</item>`;
|
|
38
|
-
}).join("");
|
|
39
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
40
|
-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
41
|
-
<channel>
|
|
42
|
-
<title>${escapeXml(siteName)}</title>
|
|
43
|
-
<link>${siteUrl}</link>
|
|
44
|
-
<description>${escapeXml(siteDescription)}</description>
|
|
45
|
-
<language>${siteLanguage}</language>
|
|
46
|
-
<atom:link href="${siteUrl}/feed" rel="self" type="application/rss+xml"/>
|
|
47
|
-
${items}
|
|
48
|
-
</channel>
|
|
49
|
-
</rss>`;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Default Atom renderer.
|
|
53
|
-
*
|
|
54
|
-
* @param data - Feed data with PostView[] (pre-computed URLs)
|
|
55
|
-
* @returns Atom XML string
|
|
56
|
-
*/ export function defaultAtomRenderer(data) {
|
|
57
|
-
const { siteName, siteDescription, siteUrl, posts } = data;
|
|
58
|
-
const entries = posts.map((post)=>{
|
|
59
|
-
const link = `${siteUrl}${post.permalink}`;
|
|
60
|
-
const title = post.title || `Post #${post.id}`;
|
|
61
|
-
return `
|
|
62
|
-
<entry>
|
|
63
|
-
<title>${escapeXml(title)}</title>
|
|
64
|
-
<link href="${link}" rel="alternate"/>
|
|
65
|
-
<id>${link}</id>
|
|
66
|
-
<published>${post.publishedAt}</published>
|
|
67
|
-
<updated>${post.updatedAt}</updated>
|
|
68
|
-
<content type="html"><![CDATA[${post.bodyHtml || ""}]]></content>
|
|
69
|
-
</entry>`;
|
|
70
|
-
}).join("");
|
|
71
|
-
const now = new Date().toISOString();
|
|
72
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
73
|
-
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
74
|
-
<title>${escapeXml(siteName)}</title>
|
|
75
|
-
<subtitle>${escapeXml(siteDescription)}</subtitle>
|
|
76
|
-
<link href="${siteUrl}" rel="alternate"/>
|
|
77
|
-
<link href="${siteUrl}/feed/atom.xml" rel="self"/>
|
|
78
|
-
<id>${siteUrl}/</id>
|
|
79
|
-
<updated>${now}</updated>
|
|
80
|
-
${entries}
|
|
81
|
-
</feed>`;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Default Sitemap renderer.
|
|
85
|
-
*
|
|
86
|
-
* @param data - Sitemap data with PostView[] and PageView[]
|
|
87
|
-
* @returns Sitemap XML string
|
|
88
|
-
*/ export function defaultSitemapRenderer(data) {
|
|
89
|
-
const { siteUrl, posts, pages } = data;
|
|
90
|
-
const postUrls = posts.map((post)=>{
|
|
91
|
-
const loc = `${siteUrl}${post.permalink}`;
|
|
92
|
-
const lastmod = post.updatedAt.split("T")[0];
|
|
93
|
-
const priority = post.featured ? "0.8" : "0.6";
|
|
94
|
-
return `
|
|
95
|
-
<url>
|
|
96
|
-
<loc>${loc}</loc>
|
|
97
|
-
<lastmod>${lastmod}</lastmod>
|
|
98
|
-
<priority>${priority}</priority>
|
|
99
|
-
</url>`;
|
|
100
|
-
}).join("");
|
|
101
|
-
const pageUrls = pages.map((page)=>{
|
|
102
|
-
const loc = `${siteUrl}/${page.slug}`;
|
|
103
|
-
const lastmod = page.updatedAt.split("T")[0];
|
|
104
|
-
return `
|
|
105
|
-
<url>
|
|
106
|
-
<loc>${loc}</loc>
|
|
107
|
-
<lastmod>${lastmod}</lastmod>
|
|
108
|
-
<priority>0.7</priority>
|
|
109
|
-
</url>`;
|
|
110
|
-
}).join("");
|
|
111
|
-
const homepageUrl = `
|
|
112
|
-
<url>
|
|
113
|
-
<loc>${siteUrl}/</loc>
|
|
114
|
-
<priority>1.0</priority>
|
|
115
|
-
<changefreq>daily</changefreq>
|
|
116
|
-
</url>`;
|
|
117
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
118
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
119
|
-
${homepageUrl}
|
|
120
|
-
${postUrls}
|
|
121
|
-
${pageUrls}
|
|
122
|
-
</urlset>`;
|
|
123
|
-
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client-side Image Processor
|
|
3
|
-
*
|
|
4
|
-
* Processes images before upload:
|
|
5
|
-
* - Corrects EXIF orientation
|
|
6
|
-
* - Resizes to max dimensions
|
|
7
|
-
* - Strips all metadata (privacy)
|
|
8
|
-
* - Converts to WebP format
|
|
9
|
-
*/ const DEFAULT_OPTIONS = {
|
|
10
|
-
maxWidth: 1920,
|
|
11
|
-
maxHeight: 1920,
|
|
12
|
-
quality: 0.85,
|
|
13
|
-
mimeType: "image/webp"
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* EXIF Orientation values and their transformations
|
|
17
|
-
*/ const ORIENTATIONS = {
|
|
18
|
-
1: {
|
|
19
|
-
rotate: 0,
|
|
20
|
-
flip: false
|
|
21
|
-
},
|
|
22
|
-
2: {
|
|
23
|
-
rotate: 0,
|
|
24
|
-
flip: true
|
|
25
|
-
},
|
|
26
|
-
3: {
|
|
27
|
-
rotate: 180,
|
|
28
|
-
flip: false
|
|
29
|
-
},
|
|
30
|
-
4: {
|
|
31
|
-
rotate: 180,
|
|
32
|
-
flip: true
|
|
33
|
-
},
|
|
34
|
-
5: {
|
|
35
|
-
rotate: 90,
|
|
36
|
-
flip: true
|
|
37
|
-
},
|
|
38
|
-
6: {
|
|
39
|
-
rotate: 90,
|
|
40
|
-
flip: false
|
|
41
|
-
},
|
|
42
|
-
7: {
|
|
43
|
-
rotate: 270,
|
|
44
|
-
flip: true
|
|
45
|
-
},
|
|
46
|
-
8: {
|
|
47
|
-
rotate: 270,
|
|
48
|
-
flip: false
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Read EXIF orientation from JPEG file
|
|
53
|
-
*/ function readExifOrientation(buffer) {
|
|
54
|
-
const view = new DataView(buffer);
|
|
55
|
-
// Check for JPEG SOI marker
|
|
56
|
-
if (view.getUint16(0) !== 0xffd8) return 1;
|
|
57
|
-
let offset = 2;
|
|
58
|
-
const length = view.byteLength;
|
|
59
|
-
while(offset < length){
|
|
60
|
-
if (view.getUint8(offset) !== 0xff) return 1;
|
|
61
|
-
const marker = view.getUint8(offset + 1);
|
|
62
|
-
// APP1 marker (EXIF)
|
|
63
|
-
if (marker === 0xe1) {
|
|
64
|
-
const exifOffset = offset + 4;
|
|
65
|
-
// Check for "Exif\0\0"
|
|
66
|
-
if (view.getUint32(exifOffset) !== 0x45786966 || view.getUint16(exifOffset + 4) !== 0x0000) {
|
|
67
|
-
return 1;
|
|
68
|
-
}
|
|
69
|
-
const tiffOffset = exifOffset + 6;
|
|
70
|
-
const littleEndian = view.getUint16(tiffOffset) === 0x4949;
|
|
71
|
-
// Validate TIFF header
|
|
72
|
-
if (view.getUint16(tiffOffset + 2, littleEndian) !== 0x002a) return 1;
|
|
73
|
-
const ifdOffset = view.getUint32(tiffOffset + 4, littleEndian);
|
|
74
|
-
const numEntries = view.getUint16(tiffOffset + ifdOffset, littleEndian);
|
|
75
|
-
// Search for orientation tag (0x0112)
|
|
76
|
-
for(let i = 0; i < numEntries; i++){
|
|
77
|
-
const entryOffset = tiffOffset + ifdOffset + 2 + i * 12;
|
|
78
|
-
const tag = view.getUint16(entryOffset, littleEndian);
|
|
79
|
-
if (tag === 0x0112) {
|
|
80
|
-
return view.getUint16(entryOffset + 8, littleEndian);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return 1;
|
|
84
|
-
}
|
|
85
|
-
// Skip to next marker
|
|
86
|
-
if (marker === 0xd8 || marker === 0xd9) {
|
|
87
|
-
offset += 2;
|
|
88
|
-
} else {
|
|
89
|
-
offset += 2 + view.getUint16(offset + 2);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return 1;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Load image from file
|
|
96
|
-
*/ function loadImage(file) {
|
|
97
|
-
return new Promise((resolve, reject)=>{
|
|
98
|
-
const img = new Image();
|
|
99
|
-
img.onload = ()=>{
|
|
100
|
-
URL.revokeObjectURL(img.src);
|
|
101
|
-
resolve(img);
|
|
102
|
-
};
|
|
103
|
-
img.onerror = ()=>reject(new Error("Failed to load image"));
|
|
104
|
-
img.src = URL.createObjectURL(file);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Calculate output dimensions maintaining aspect ratio
|
|
109
|
-
*/ function calculateDimensions(width, height, maxWidth, maxHeight) {
|
|
110
|
-
if (width <= maxWidth && height <= maxHeight) {
|
|
111
|
-
return {
|
|
112
|
-
width,
|
|
113
|
-
height
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
117
|
-
return {
|
|
118
|
-
width: Math.round(width * ratio),
|
|
119
|
-
height: Math.round(height * ratio)
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Process image file
|
|
124
|
-
*/ async function process(file, options = {}) {
|
|
125
|
-
const opts = {
|
|
126
|
-
...DEFAULT_OPTIONS,
|
|
127
|
-
...options
|
|
128
|
-
};
|
|
129
|
-
// Read file buffer for EXIF
|
|
130
|
-
const buffer = await file.arrayBuffer();
|
|
131
|
-
const orientation = readExifOrientation(buffer);
|
|
132
|
-
const transform = ORIENTATIONS[orientation] || ORIENTATIONS[1];
|
|
133
|
-
// Load image
|
|
134
|
-
const img = await loadImage(file);
|
|
135
|
-
// For 90° or 270° rotation, swap dimensions
|
|
136
|
-
const isRotated = transform.rotate === 90 || transform.rotate === 270;
|
|
137
|
-
const srcWidth = isRotated ? img.height : img.width;
|
|
138
|
-
const srcHeight = isRotated ? img.width : img.height;
|
|
139
|
-
// Calculate output size
|
|
140
|
-
const { width, height } = calculateDimensions(srcWidth, srcHeight, opts.maxWidth, opts.maxHeight);
|
|
141
|
-
// Create canvas
|
|
142
|
-
const canvas = document.createElement("canvas");
|
|
143
|
-
canvas.width = width;
|
|
144
|
-
canvas.height = height;
|
|
145
|
-
const ctx = canvas.getContext("2d");
|
|
146
|
-
if (!ctx) throw new Error("Failed to get canvas context");
|
|
147
|
-
// Apply transformations
|
|
148
|
-
ctx.save();
|
|
149
|
-
ctx.translate(width / 2, height / 2);
|
|
150
|
-
if (transform.rotate) {
|
|
151
|
-
ctx.rotate(transform.rotate * Math.PI / 180);
|
|
152
|
-
}
|
|
153
|
-
if (transform.flip) {
|
|
154
|
-
ctx.scale(-1, 1);
|
|
155
|
-
}
|
|
156
|
-
const drawWidth = isRotated ? height : width;
|
|
157
|
-
const drawHeight = isRotated ? width : height;
|
|
158
|
-
ctx.drawImage(img, -drawWidth / 2, -drawHeight / 2, drawWidth, drawHeight);
|
|
159
|
-
ctx.restore();
|
|
160
|
-
// Export as WebP
|
|
161
|
-
return new Promise((resolve, reject)=>{
|
|
162
|
-
canvas.toBlob((blob)=>{
|
|
163
|
-
if (blob) {
|
|
164
|
-
resolve(blob);
|
|
165
|
-
} else {
|
|
166
|
-
reject(new Error("Failed to create blob"));
|
|
167
|
-
}
|
|
168
|
-
}, opts.mimeType, opts.quality);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Process file and create a new File object
|
|
173
|
-
*/ async function processToFile(file, options = {}) {
|
|
174
|
-
const blob = await process(file, options);
|
|
175
|
-
// Generate new filename with .webp extension
|
|
176
|
-
const originalName = file.name.replace(/\.[^.]+$/, "");
|
|
177
|
-
const newName = `${originalName}.webp`;
|
|
178
|
-
return new File([
|
|
179
|
-
blob
|
|
180
|
-
], newName, {
|
|
181
|
-
type: "image/webp"
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
export const ImageProcessor = {
|
|
185
|
-
process,
|
|
186
|
-
processToFile
|
|
187
|
-
};
|
package/dist/lib/image.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image URL utilities
|
|
3
|
-
*
|
|
4
|
-
* Provides helpers for generating image URLs with optional transformations.
|
|
5
|
-
*/ /**
|
|
6
|
-
* Options for image transformations
|
|
7
|
-
*/ /**
|
|
8
|
-
* Generates an image URL with optional transformations.
|
|
9
|
-
*
|
|
10
|
-
* If `transformUrl` is provided and options are specified, returns a transformed image URL.
|
|
11
|
-
* Otherwise, returns the original URL unchanged.
|
|
12
|
-
*
|
|
13
|
-
* Compatible with:
|
|
14
|
-
* - Cloudflare Image Transformations (`/cdn-cgi/image/...`)
|
|
15
|
-
* - imgproxy
|
|
16
|
-
* - Cloudinary
|
|
17
|
-
* - Any service with similar URL-based transformation API
|
|
18
|
-
*
|
|
19
|
-
* @param originalUrl - The original image URL
|
|
20
|
-
* @param transformUrl - The base URL for transformations (e.g., `https://example.com/cdn-cgi/image`)
|
|
21
|
-
* @param options - Transformation options (width, height, quality, format, fit)
|
|
22
|
-
* @returns The transformed URL or original URL if transformations are not configured
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* // Without transform URL - returns original
|
|
27
|
-
* getImageUrl("/media/abc123", undefined, { width: 200 });
|
|
28
|
-
* // Returns: "/media/abc123"
|
|
29
|
-
*
|
|
30
|
-
* // With transform URL - returns transformed
|
|
31
|
-
* getImageUrl("/media/abc123", "https://example.com/cdn-cgi/image", { width: 200, quality: 80 });
|
|
32
|
-
* // Returns: "https://example.com/cdn-cgi/image/width=200,quality=80/https://example.com/media/abc123"
|
|
33
|
-
* ```
|
|
34
|
-
*/ export function getImageUrl(originalUrl, transformUrl, options) {
|
|
35
|
-
if (!transformUrl || !options || Object.keys(options).length === 0) {
|
|
36
|
-
return originalUrl;
|
|
37
|
-
}
|
|
38
|
-
const params = [];
|
|
39
|
-
if (options.width) params.push(`width=${options.width}`);
|
|
40
|
-
if (options.height) params.push(`height=${options.height}`);
|
|
41
|
-
if (options.quality) params.push(`quality=${options.quality}`);
|
|
42
|
-
if (options.format) params.push(`format=${options.format}`);
|
|
43
|
-
if (options.fit) params.push(`fit=${options.fit}`);
|
|
44
|
-
if (params.length === 0) {
|
|
45
|
-
return originalUrl;
|
|
46
|
-
}
|
|
47
|
-
return `${transformUrl}/${params.join(",")}/${originalUrl}`;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Returns the appropriate public URL base for a given storage provider.
|
|
51
|
-
*
|
|
52
|
-
* For `"s3"` provider, returns `s3PublicUrl`. For all other providers
|
|
53
|
-
* (including `"r2"`), returns `r2PublicUrl`. Falls back to `undefined`
|
|
54
|
-
* if the matching URL is not configured.
|
|
55
|
-
*
|
|
56
|
-
* @param provider - The storage provider identifier (e.g., `"r2"`, `"s3"`)
|
|
57
|
-
* @param r2PublicUrl - Optional R2 public URL
|
|
58
|
-
* @param s3PublicUrl - Optional S3 public URL
|
|
59
|
-
* @returns The public URL base for the provider, or undefined
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
|
-
* ```ts
|
|
63
|
-
* getPublicUrlForProvider("r2", "https://r2.example.com", "https://s3.example.com");
|
|
64
|
-
* // Returns: "https://r2.example.com"
|
|
65
|
-
*
|
|
66
|
-
* getPublicUrlForProvider("s3", "https://r2.example.com", "https://s3.example.com");
|
|
67
|
-
* // Returns: "https://s3.example.com"
|
|
68
|
-
* ```
|
|
69
|
-
*/ export function getPublicUrlForProvider(provider, r2PublicUrl, s3PublicUrl) {
|
|
70
|
-
if (provider === "s3") return s3PublicUrl;
|
|
71
|
-
return r2PublicUrl;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Generates a media URL from a storage key.
|
|
75
|
-
*
|
|
76
|
-
* Both proxy and CDN paths use the same structure — only the domain differs.
|
|
77
|
-
* Without a public URL, returns a root-relative path for the local proxy.
|
|
78
|
-
* With a public URL, prefixes that domain.
|
|
79
|
-
*
|
|
80
|
-
* @param storageKey - The storage object key (e.g. `"media/2025/01/uuid.webp"`)
|
|
81
|
-
* @param publicUrl - Optional public URL base for direct CDN access
|
|
82
|
-
* @returns The public URL for the media file
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* ```ts
|
|
86
|
-
* // Without public URL - local proxy
|
|
87
|
-
* getMediaUrl("media/2025/01/01902a9f-1a2b-7c3d.webp");
|
|
88
|
-
* // Returns: "/media/2025/01/01902a9f-1a2b-7c3d.webp"
|
|
89
|
-
*
|
|
90
|
-
* // With public URL - CDN
|
|
91
|
-
* getMediaUrl("media/2025/01/01902a9f-1a2b-7c3d.webp", "https://cdn.example.com");
|
|
92
|
-
* // Returns: "https://cdn.example.com/media/2025/01/01902a9f-1a2b-7c3d.webp"
|
|
93
|
-
* ```
|
|
94
|
-
*/ export function getMediaUrl(storageKey, publicUrl) {
|
|
95
|
-
const base = publicUrl ? publicUrl.replace(/\/+$/, "") : "";
|
|
96
|
-
return `${base}/${storageKey}`;
|
|
97
|
-
}
|
package/dist/lib/index.js
DELETED
package/dist/lib/markdown.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markdown Rendering
|
|
3
|
-
*
|
|
4
|
-
* Uses marked with minimal configuration
|
|
5
|
-
*/ import { marked } from "marked";
|
|
6
|
-
// Configure marked for security and simplicity
|
|
7
|
-
marked.setOptions({
|
|
8
|
-
gfm: true,
|
|
9
|
-
breaks: true
|
|
10
|
-
});
|
|
11
|
-
/**
|
|
12
|
-
* Renders Markdown content to HTML using the marked library.
|
|
13
|
-
*
|
|
14
|
-
* Configured with GitHub Flavored Markdown (GFM) support and line breaks enabled.
|
|
15
|
-
* Uses synchronous parsing for simplicity and consistency in server-side rendering.
|
|
16
|
-
*
|
|
17
|
-
* @param markdown - The Markdown string to convert to HTML
|
|
18
|
-
* @returns The rendered HTML string
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* const html = render("# Hello\n\nThis is **bold** text.");
|
|
23
|
-
* // Returns: "<h1>Hello</h1>\n<p>This is <strong>bold</strong> text.</p>"
|
|
24
|
-
* ```
|
|
25
|
-
*/ export function render(markdown) {
|
|
26
|
-
return marked.parse(markdown, {
|
|
27
|
-
async: false
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Converts Markdown to plain text by stripping all formatting syntax.
|
|
32
|
-
*
|
|
33
|
-
* Removes Markdown syntax including headers, bold, italic, links, images, code blocks,
|
|
34
|
-
* blockquotes, lists, and converts newlines to spaces. Useful for generating text excerpts,
|
|
35
|
-
* meta descriptions, or search indexes.
|
|
36
|
-
*
|
|
37
|
-
* @param markdown - The Markdown string to convert to plain text
|
|
38
|
-
* @returns The plain text string with all Markdown syntax removed
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```ts
|
|
42
|
-
* const plain = toPlainText("## Hello\n\nThis is **bold** and [a link](url).");
|
|
43
|
-
* // Returns: "Hello This is bold and a link."
|
|
44
|
-
* ```
|
|
45
|
-
*/ export function toPlainText(markdown) {
|
|
46
|
-
return markdown.replace(/#{1,6}\s+/g, "") // Remove headers
|
|
47
|
-
.replace(/\*\*(.+?)\*\*/g, "$1") // Bold
|
|
48
|
-
.replace(/\*(.+?)\*/g, "$1") // Italic
|
|
49
|
-
.replace(/\[(.+?)\]\(.+?\)/g, "$1") // Links
|
|
50
|
-
.replace(/!\[.*?\]\(.+?\)/g, "") // Images
|
|
51
|
-
.replace(/`{1,3}[^`]*`{1,3}/g, "") // Code
|
|
52
|
-
.replace(/>\s+/g, "") // Blockquotes
|
|
53
|
-
.replace(/[-*+]\s+/g, "") // Lists
|
|
54
|
-
.replace(/\n+/g, " ") // Newlines
|
|
55
|
-
.trim();
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Extracts a title from Markdown content by taking the first sentence or line.
|
|
59
|
-
*
|
|
60
|
-
* Converts Markdown to plain text first, then takes the first sentence (split by `.!?`)
|
|
61
|
-
* or truncates to the specified maximum length. Useful for generating automatic titles
|
|
62
|
-
* from post content when no explicit title is provided.
|
|
63
|
-
*
|
|
64
|
-
* @param markdown - The Markdown string to extract a title from
|
|
65
|
-
* @param maxLength - Maximum length of the extracted title (default: 120)
|
|
66
|
-
* @returns The extracted title string, with "..." appended if truncated
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```ts
|
|
70
|
-
* const title = extractTitle("This is the first sentence. And another one.", 50);
|
|
71
|
-
* // Returns: "This is the first sentence"
|
|
72
|
-
*
|
|
73
|
-
* const title = extractTitle("A very long sentence that exceeds the maximum length...", 30);
|
|
74
|
-
* // Returns: "A very long sentence that ex..."
|
|
75
|
-
* ```
|
|
76
|
-
*/ export function extractTitle(markdown, maxLength = 120) {
|
|
77
|
-
const plain = toPlainText(markdown);
|
|
78
|
-
const firstLine = plain.split(/[.!?]/)[0] ?? plain;
|
|
79
|
-
if (firstLine.length <= maxLength) {
|
|
80
|
-
return firstLine;
|
|
81
|
-
}
|
|
82
|
-
return plain.slice(0, maxLength).trim() + "...";
|
|
83
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Media Helper Utilities
|
|
3
|
-
*
|
|
4
|
-
* Shared logic for building MediaAttachment maps from raw media data.
|
|
5
|
-
*/ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
6
|
-
/**
|
|
7
|
-
* Builds a map of post IDs to MediaAttachment arrays from raw media data.
|
|
8
|
-
*
|
|
9
|
-
* Transforms raw Media objects (with storage keys) into MediaAttachment objects
|
|
10
|
-
* (with public URLs and preview URLs) suitable for rendering.
|
|
11
|
-
* Automatically resolves the correct public URL based on each media item's
|
|
12
|
-
* storage provider (`"r2"` or `"s3"`).
|
|
13
|
-
*
|
|
14
|
-
* @param rawMediaMap - Map of post IDs to raw Media arrays from the media service
|
|
15
|
-
* @param r2PublicUrl - Optional R2 public URL for direct CDN access
|
|
16
|
-
* @param imageTransformUrl - Optional image transformation service URL
|
|
17
|
-
* @param s3PublicUrl - Optional S3 public URL for direct CDN access
|
|
18
|
-
* @returns Map of post IDs to MediaAttachment arrays
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* const rawMediaMap = await services.media.getByPostIds(postIds);
|
|
23
|
-
* const mediaMap = buildMediaMap(rawMediaMap, c.env.R2_PUBLIC_URL, c.env.IMAGE_TRANSFORM_URL, c.env.S3_PUBLIC_URL);
|
|
24
|
-
* ```
|
|
25
|
-
*/ export function buildMediaMap(rawMediaMap, r2PublicUrl, imageTransformUrl, s3PublicUrl) {
|
|
26
|
-
const mediaMap = new Map();
|
|
27
|
-
for (const [postId, mediaList] of rawMediaMap){
|
|
28
|
-
mediaMap.set(postId, mediaList.map((m)=>{
|
|
29
|
-
const publicUrl = getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl);
|
|
30
|
-
return {
|
|
31
|
-
id: m.id,
|
|
32
|
-
url: getMediaUrl(m.storageKey, publicUrl),
|
|
33
|
-
previewUrl: getImageUrl(getMediaUrl(m.storageKey, publicUrl), imageTransformUrl, {
|
|
34
|
-
width: 400,
|
|
35
|
-
quality: 80,
|
|
36
|
-
format: "auto",
|
|
37
|
-
fit: "cover"
|
|
38
|
-
}),
|
|
39
|
-
alt: m.alt,
|
|
40
|
-
blurhash: m.blurhash,
|
|
41
|
-
width: m.width,
|
|
42
|
-
height: m.height,
|
|
43
|
-
position: m.position,
|
|
44
|
-
mimeType: m.mimeType
|
|
45
|
-
};
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
48
|
-
return mediaMap;
|
|
49
|
-
}
|