@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/icons.ts
CHANGED
|
@@ -93,7 +93,8 @@ export function parseCollectionIcon(
|
|
|
93
93
|
if (
|
|
94
94
|
typeof parsed.name === "string" &&
|
|
95
95
|
typeof parsed.svg === "string" &&
|
|
96
|
-
typeof parsed.color === "string"
|
|
96
|
+
typeof parsed.color === "string" &&
|
|
97
|
+
/^#[0-9a-f]{3,6}$/i.test(parsed.color)
|
|
97
98
|
) {
|
|
98
99
|
return parsed as unknown as CollectionIcon;
|
|
99
100
|
}
|
|
@@ -186,7 +187,7 @@ function applyIconSize(svg: string, size: number, color?: string): string {
|
|
|
186
187
|
let result = svg
|
|
187
188
|
.replace(/width="24"/, `width="${size}"`)
|
|
188
189
|
.replace(/height="24"/, `height="${size}"`);
|
|
189
|
-
if (color) {
|
|
190
|
+
if (color && /^#[0-9a-f]{3,6}$/i.test(color)) {
|
|
190
191
|
result = result.replace("<svg", `<svg style="color: ${color}"`);
|
|
191
192
|
}
|
|
192
193
|
return result;
|
package/src/lib/index.ts
CHANGED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → TipTap JSON Conversion
|
|
3
|
+
*
|
|
4
|
+
* Converts Markdown strings to TipTap JSON documents using `marked.lexer()`
|
|
5
|
+
* for tokenization. Enables the API to accept Markdown while the internal
|
|
6
|
+
* pipeline (renderTiptapJson / extractBodyText / extractSummary) continues
|
|
7
|
+
* to operate on TipTap JSON.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { marked, type Token, type Tokens } from "marked";
|
|
11
|
+
|
|
12
|
+
interface TiptapMark {
|
|
13
|
+
type: string;
|
|
14
|
+
attrs?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TiptapNode {
|
|
18
|
+
type: string;
|
|
19
|
+
content?: TiptapNode[];
|
|
20
|
+
text?: string;
|
|
21
|
+
marks?: TiptapMark[];
|
|
22
|
+
attrs?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Converts a Markdown string to a TipTap JSON document string.
|
|
27
|
+
*
|
|
28
|
+
* Uses `marked.lexer()` to tokenize, then maps each token to the
|
|
29
|
+
* corresponding TipTap node structure that `renderTiptapJson()` expects.
|
|
30
|
+
*
|
|
31
|
+
* @param markdown - Markdown source text
|
|
32
|
+
* @returns Stringified TipTap JSON document
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const json = markdownToTiptapJson("Hello **world**");
|
|
37
|
+
* // '{"type":"doc","content":[{"type":"paragraph","content":[...]}]}'
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function markdownToTiptapJson(markdown: string): string {
|
|
41
|
+
const tokens = marked.lexer(markdown, { gfm: true, breaks: true });
|
|
42
|
+
const content = tokens.flatMap(blockTokenToNodes);
|
|
43
|
+
// Ensure at least one node so the doc is valid
|
|
44
|
+
if (content.length === 0) {
|
|
45
|
+
content.push({ type: "paragraph" });
|
|
46
|
+
}
|
|
47
|
+
const doc: TiptapNode = { type: "doc", content };
|
|
48
|
+
return JSON.stringify(doc);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Block-level token → TipTap node mapping
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
function blockTokenToNodes(token: Token): TiptapNode[] {
|
|
56
|
+
switch (token.type) {
|
|
57
|
+
case "paragraph":
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
type: "paragraph",
|
|
61
|
+
content: inlineTokensToNodes(
|
|
62
|
+
(token as Tokens.Paragraph).tokens ?? [],
|
|
63
|
+
[],
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Tight list items use "text" instead of "paragraph" as the block wrapper
|
|
69
|
+
case "text": {
|
|
70
|
+
const t = token as Tokens.Text;
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
type: "paragraph",
|
|
74
|
+
content: inlineTokensToNodes(t.tokens ?? [], []),
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case "heading": {
|
|
80
|
+
const t = token as Tokens.Heading;
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
type: "heading",
|
|
84
|
+
attrs: { level: t.depth },
|
|
85
|
+
content: inlineTokensToNodes(t.tokens ?? [], []),
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "code": {
|
|
91
|
+
const t = token as Tokens.Code;
|
|
92
|
+
const node: TiptapNode = {
|
|
93
|
+
type: "codeBlock",
|
|
94
|
+
content: [{ type: "text", text: t.text }],
|
|
95
|
+
};
|
|
96
|
+
if (t.lang) {
|
|
97
|
+
node.attrs = { language: t.lang };
|
|
98
|
+
}
|
|
99
|
+
return [node];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case "blockquote": {
|
|
103
|
+
const t = token as Tokens.Blockquote;
|
|
104
|
+
const inner = (t.tokens ?? []).flatMap(blockTokenToNodes);
|
|
105
|
+
return [{ type: "blockquote", content: inner }];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case "list": {
|
|
109
|
+
const t = token as Tokens.List;
|
|
110
|
+
const listType = t.ordered ? "orderedList" : "bulletList";
|
|
111
|
+
const items = t.items.map(listItemToNode);
|
|
112
|
+
const node: TiptapNode = { type: listType, content: items };
|
|
113
|
+
if (t.ordered && t.start !== undefined && t.start !== 1) {
|
|
114
|
+
node.attrs = { start: t.start };
|
|
115
|
+
}
|
|
116
|
+
return [node];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case "hr":
|
|
120
|
+
return [{ type: "horizontalRule" }];
|
|
121
|
+
|
|
122
|
+
case "html": {
|
|
123
|
+
const t = token as Tokens.HTML;
|
|
124
|
+
if (t.text.trim() === "<!--more-->") {
|
|
125
|
+
return [{ type: "moreBreak" }];
|
|
126
|
+
}
|
|
127
|
+
// Other raw HTML: wrap in a paragraph as plain text
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
type: "paragraph",
|
|
131
|
+
content: [{ type: "text", text: t.text.trim() }],
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case "table": {
|
|
137
|
+
const t = token as Tokens.Table;
|
|
138
|
+
const rows: TiptapNode[] = [];
|
|
139
|
+
|
|
140
|
+
// Header row
|
|
141
|
+
const headerCells = t.header.map(
|
|
142
|
+
(cell): TiptapNode => ({
|
|
143
|
+
type: "tableHeader",
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "paragraph",
|
|
147
|
+
content: inlineTokensToNodes(cell.tokens, []),
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
rows.push({ type: "tableRow", content: headerCells });
|
|
153
|
+
|
|
154
|
+
// Body rows
|
|
155
|
+
for (const row of t.rows) {
|
|
156
|
+
const cells = row.map(
|
|
157
|
+
(cell): TiptapNode => ({
|
|
158
|
+
type: "tableCell",
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "paragraph",
|
|
162
|
+
content: inlineTokensToNodes(cell.tokens, []),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
rows.push({ type: "tableRow", content: cells });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return [{ type: "table", content: rows }];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case "space":
|
|
174
|
+
return [];
|
|
175
|
+
|
|
176
|
+
default:
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function listItemToNode(item: Tokens.ListItem): TiptapNode {
|
|
182
|
+
// A list item's tokens can be block-level (loose list) or inline
|
|
183
|
+
const children = (item.tokens ?? []).flatMap(blockTokenToNodes);
|
|
184
|
+
return { type: "listItem", content: children };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Inline token → TipTap node mapping (flattened marks model)
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
function inlineTokensToNodes(
|
|
192
|
+
tokens: Token[],
|
|
193
|
+
marks: TiptapMark[],
|
|
194
|
+
): TiptapNode[] {
|
|
195
|
+
const nodes: TiptapNode[] = [];
|
|
196
|
+
|
|
197
|
+
for (const token of tokens) {
|
|
198
|
+
switch (token.type) {
|
|
199
|
+
case "text": {
|
|
200
|
+
const t = token as Tokens.Text;
|
|
201
|
+
// marked may nest inline tokens inside text tokens
|
|
202
|
+
if (t.tokens && t.tokens.length > 0) {
|
|
203
|
+
nodes.push(...inlineTokensToNodes(t.tokens, marks));
|
|
204
|
+
} else {
|
|
205
|
+
const textNode: TiptapNode = { type: "text", text: t.text };
|
|
206
|
+
if (marks.length > 0) textNode.marks = [...marks];
|
|
207
|
+
nodes.push(textNode);
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case "strong": {
|
|
213
|
+
const t = token as Tokens.Strong;
|
|
214
|
+
const newMarks = [...marks, { type: "bold" }];
|
|
215
|
+
nodes.push(...inlineTokensToNodes(t.tokens ?? [], newMarks));
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case "em": {
|
|
220
|
+
const t = token as Tokens.Em;
|
|
221
|
+
const newMarks = [...marks, { type: "italic" }];
|
|
222
|
+
nodes.push(...inlineTokensToNodes(t.tokens ?? [], newMarks));
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case "codespan": {
|
|
227
|
+
const t = token as Tokens.Codespan;
|
|
228
|
+
const textNode: TiptapNode = { type: "text", text: t.text };
|
|
229
|
+
textNode.marks = [...marks, { type: "code" }];
|
|
230
|
+
nodes.push(textNode);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case "del": {
|
|
235
|
+
const t = token as Tokens.Del;
|
|
236
|
+
const newMarks = [...marks, { type: "strike" }];
|
|
237
|
+
nodes.push(...inlineTokensToNodes(t.tokens ?? [], newMarks));
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case "link": {
|
|
242
|
+
const t = token as Tokens.Link;
|
|
243
|
+
const linkMark: TiptapMark = {
|
|
244
|
+
type: "link",
|
|
245
|
+
attrs: { href: t.href, target: "_blank" },
|
|
246
|
+
};
|
|
247
|
+
const newMarks = [...marks, linkMark];
|
|
248
|
+
nodes.push(...inlineTokensToNodes(t.tokens ?? [], newMarks));
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case "image": {
|
|
253
|
+
const t = token as Tokens.Image;
|
|
254
|
+
const imgAttrs: Record<string, unknown> = { src: t.href };
|
|
255
|
+
if (t.text) imgAttrs.alt = t.text;
|
|
256
|
+
if (t.title) imgAttrs.title = t.title;
|
|
257
|
+
const imgNode: TiptapNode = { type: "image", attrs: imgAttrs };
|
|
258
|
+
nodes.push(imgNode);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case "br":
|
|
263
|
+
nodes.push({ type: "hardBreak" });
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case "escape": {
|
|
267
|
+
const t = token as Tokens.Escape;
|
|
268
|
+
const textNode: TiptapNode = { type: "text", text: t.text };
|
|
269
|
+
if (marks.length > 0) textNode.marks = [...marks];
|
|
270
|
+
nodes.push(textNode);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
default:
|
|
275
|
+
// For any unhandled inline token with raw text, emit as text
|
|
276
|
+
if ("text" in token && typeof token.text === "string") {
|
|
277
|
+
const textNode: TiptapNode = { type: "text", text: token.text };
|
|
278
|
+
if (marks.length > 0) textNode.marks = [...marks];
|
|
279
|
+
nodes.push(textNode);
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return nodes;
|
|
286
|
+
}
|
package/src/lib/media-helpers.ts
CHANGED
|
@@ -28,12 +28,12 @@ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
export function buildMediaMap(
|
|
31
|
-
rawMediaMap: Map<
|
|
31
|
+
rawMediaMap: Map<string, Media[]>,
|
|
32
32
|
r2PublicUrl?: string,
|
|
33
33
|
imageTransformUrl?: string,
|
|
34
34
|
s3PublicUrl?: string,
|
|
35
|
-
): Map<
|
|
36
|
-
const mediaMap = new Map<
|
|
35
|
+
): Map<string, MediaAttachment[]> {
|
|
36
|
+
const mediaMap = new Map<string, MediaAttachment[]>();
|
|
37
37
|
for (const [postId, mediaList] of rawMediaMap) {
|
|
38
38
|
mediaMap.set(
|
|
39
39
|
postId,
|
|
@@ -43,26 +43,36 @@ export function buildMediaMap(
|
|
|
43
43
|
r2PublicUrl,
|
|
44
44
|
s3PublicUrl,
|
|
45
45
|
);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
getMediaUrl(m.storageKey, publicUrl),
|
|
51
|
-
imageTransformUrl,
|
|
52
|
-
{
|
|
46
|
+
const mediaUrl = getMediaUrl(m.storageKey, publicUrl);
|
|
47
|
+
// Only apply image transforms for image MIME types
|
|
48
|
+
const previewUrl = m.mimeType.startsWith("image/")
|
|
49
|
+
? getImageUrl(mediaUrl, imageTransformUrl, {
|
|
53
50
|
width: 1200,
|
|
54
51
|
height: 768,
|
|
55
52
|
quality: 80,
|
|
56
53
|
format: "auto",
|
|
57
54
|
fit: "scale-down",
|
|
58
|
-
}
|
|
59
|
-
|
|
55
|
+
})
|
|
56
|
+
: mediaUrl;
|
|
57
|
+
const posterUrl = m.posterKey
|
|
58
|
+
? getMediaUrl(m.posterKey, publicUrl)
|
|
59
|
+
: null;
|
|
60
|
+
return {
|
|
61
|
+
id: m.id,
|
|
62
|
+
url: mediaUrl,
|
|
63
|
+
previewUrl,
|
|
60
64
|
alt: m.alt,
|
|
61
65
|
blurhash: m.blurhash,
|
|
66
|
+
waveform: m.waveform,
|
|
67
|
+
posterUrl,
|
|
62
68
|
width: m.width,
|
|
63
69
|
height: m.height,
|
|
64
70
|
position: m.position,
|
|
65
71
|
mimeType: m.mimeType,
|
|
72
|
+
originalName: m.originalName,
|
|
73
|
+
size: m.size,
|
|
74
|
+
summary: m.summary,
|
|
75
|
+
chars: m.chars,
|
|
66
76
|
};
|
|
67
77
|
}),
|
|
68
78
|
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Random ID Generation
|
|
3
|
+
*
|
|
4
|
+
* Wraps nanoid's `customAlphabet` to produce short, URL-safe random IDs
|
|
5
|
+
* using a lowercase alphanumeric alphabet (0-9, a-z).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { customAlphabet } from "nanoid";
|
|
9
|
+
|
|
10
|
+
const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a random ID of the given length using lowercase alphanumeric characters.
|
|
14
|
+
*
|
|
15
|
+
* Uses nanoid's `customAlphabet` for uniform distribution.
|
|
16
|
+
*
|
|
17
|
+
* @param length - Number of characters in the generated ID
|
|
18
|
+
* @returns Random alphanumeric string
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* generateRandomId(5); // e.g. "a3k9m"
|
|
23
|
+
* generateRandomId(8); // e.g. "b7x2q4fn"
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function generateRandomId(length: number): string {
|
|
27
|
+
const generate = customAlphabet(ALPHABET, length);
|
|
28
|
+
return generate();
|
|
29
|
+
}
|
package/src/lib/navigation.ts
CHANGED
|
@@ -83,7 +83,7 @@ export async function getNavigationData(c: Context): Promise<NavigationData> {
|
|
|
83
83
|
|
|
84
84
|
// Only load collections when authenticated (for compose dialog)
|
|
85
85
|
if (isAuthenticated) {
|
|
86
|
-
collections = await c.var.services.collections.
|
|
86
|
+
collections = await c.var.services.collections.listByRecentActivity();
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
return {
|
package/src/lib/render.tsx
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { Context } from "hono";
|
|
9
9
|
import type { Child } from "hono/jsx";
|
|
10
10
|
import type { SiteLayoutProps } from "../types.js";
|
|
11
|
-
import { BaseLayout } from "../ui/layouts/BaseLayout.js";
|
|
11
|
+
import { BaseLayout, type ToastProps } from "../ui/layouts/BaseLayout.js";
|
|
12
12
|
import { SiteLayout } from "../ui/layouts/SiteLayout.js";
|
|
13
13
|
import type { NavigationData } from "./navigation.js";
|
|
14
14
|
|
|
@@ -23,6 +23,12 @@ export interface RenderPublicPageOptions {
|
|
|
23
23
|
content: Child;
|
|
24
24
|
/** Optional sidebar content for sidebar layout */
|
|
25
25
|
sidebar?: Child;
|
|
26
|
+
/** Optional toast notification */
|
|
27
|
+
toast?: ToastProps;
|
|
28
|
+
/** Whether to render the shared compose dialog shell */
|
|
29
|
+
showComposeDialog?: boolean;
|
|
30
|
+
/** Whether to render the site header */
|
|
31
|
+
showHeader?: boolean;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
/**
|
|
@@ -43,11 +49,23 @@ export interface RenderPublicPageOptions {
|
|
|
43
49
|
* ```
|
|
44
50
|
*/
|
|
45
51
|
export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
46
|
-
const {
|
|
52
|
+
const {
|
|
53
|
+
title,
|
|
54
|
+
description,
|
|
55
|
+
navData,
|
|
56
|
+
content,
|
|
57
|
+
sidebar,
|
|
58
|
+
toast,
|
|
59
|
+
showComposeDialog,
|
|
60
|
+
showHeader,
|
|
61
|
+
} = options;
|
|
47
62
|
|
|
48
63
|
// Use siteDescription as meta description fallback when not explicitly provided
|
|
49
64
|
const metaDescription = description || navData.siteDescription || undefined;
|
|
50
65
|
|
|
66
|
+
// Read favicon, version, and noindex from appConfig
|
|
67
|
+
const appConfig = c.get("appConfig");
|
|
68
|
+
|
|
51
69
|
const layoutProps: SiteLayoutProps = {
|
|
52
70
|
siteName: navData.siteName,
|
|
53
71
|
links: navData.links,
|
|
@@ -60,10 +78,10 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
60
78
|
showHeaderAvatar: navData.showHeaderAvatar,
|
|
61
79
|
siteFooterHtml: navData.siteFooterHtml,
|
|
62
80
|
sidebar,
|
|
81
|
+
uploadMaxFileSize: appConfig.uploadMaxFileSize,
|
|
82
|
+
showComposeDialog,
|
|
83
|
+
showHeader,
|
|
63
84
|
};
|
|
64
|
-
|
|
65
|
-
// Read favicon, version, and noindex from appConfig
|
|
66
|
-
const appConfig = c.get("appConfig");
|
|
67
85
|
const faviconUrl = appConfig.siteAvatarUrl || undefined;
|
|
68
86
|
const faviconVersion = appConfig.faviconVersion || undefined;
|
|
69
87
|
const noindex = appConfig.noindex;
|
|
@@ -77,6 +95,7 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
77
95
|
faviconVersion={faviconVersion}
|
|
78
96
|
noindex={noindex}
|
|
79
97
|
isAuthenticated={navData.isAuthenticated}
|
|
98
|
+
toast={toast}
|
|
80
99
|
>
|
|
81
100
|
<SiteLayout {...layoutProps}>{content}</SiteLayout>
|
|
82
101
|
</BaseLayout>,
|
|
@@ -117,7 +117,7 @@ export function resolveConfig(
|
|
|
117
117
|
resolve("HEADER_NAV_MAX_VISIBLE", allSettings, env),
|
|
118
118
|
10,
|
|
119
119
|
);
|
|
120
|
-
return Math.max(0, Math.min(5, isNaN(parsed) ?
|
|
120
|
+
return Math.max(0, Math.min(5, isNaN(parsed) ? 2 : parsed));
|
|
121
121
|
})(),
|
|
122
122
|
timeZone: resolve("TIME_ZONE", allSettings, env),
|
|
123
123
|
siteFooter: resolve("SITE_FOOTER", allSettings, env),
|
|
@@ -133,6 +133,17 @@ export function resolveConfig(
|
|
|
133
133
|
s3PublicUrl,
|
|
134
134
|
imageTransformUrl,
|
|
135
135
|
|
|
136
|
+
// Upload (ENV only)
|
|
137
|
+
uploadMaxFileSize:
|
|
138
|
+
parseInt(env.UPLOAD_MAX_FILE_SIZE_MB ?? "500", 10) || 500,
|
|
139
|
+
|
|
140
|
+
// Summary extraction (ENV only)
|
|
141
|
+
summaryMaxParagraphs: parseInt(env.SUMMARY_MAX_PARAGRAPHS ?? "5", 10) || 5,
|
|
142
|
+
summaryMaxChars: parseInt(env.SUMMARY_MAX_CHARS ?? "500", 10) || 500,
|
|
143
|
+
|
|
144
|
+
// Slug (ENV only)
|
|
145
|
+
slugIdLength: parseInt(env.SLUG_ID_LENGTH ?? "5", 10) || 5,
|
|
146
|
+
|
|
136
147
|
// Pagination/Feed (ENV only)
|
|
137
148
|
pageSize: parseInt(env.PAGE_SIZE ?? "20", 10) || 20,
|
|
138
149
|
rssFeedLimit: parseInt(env.RSS_FEED_LIMIT ?? "50", 10) || 50,
|
|
@@ -154,7 +165,7 @@ export function resolveConfig(
|
|
|
154
165
|
siteAvatarUrl,
|
|
155
166
|
faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
|
|
156
167
|
|
|
157
|
-
//
|
|
168
|
+
// Settings form placeholders (ENV > Default, without DB)
|
|
158
169
|
fallbacks: {
|
|
159
170
|
siteName: resolveFallback("SITE_NAME", env),
|
|
160
171
|
siteDescription: resolveFallback("SITE_DESCRIPTION", env),
|