@jant/core 0.3.36 → 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/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- 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/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- 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 -145
- 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 +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- 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__/schemas.test.ts +181 -63
- 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__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- 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 +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- 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 +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- 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/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -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 +51 -42
- 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 +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- 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 +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- 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 +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- 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 +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- 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 +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- 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 +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- 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 +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- 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/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- 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/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- 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/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- 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 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
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,
|
|
@@ -54,16 +54,25 @@ export function buildMediaMap(
|
|
|
54
54
|
fit: "scale-down",
|
|
55
55
|
})
|
|
56
56
|
: mediaUrl;
|
|
57
|
+
const posterUrl = m.posterKey
|
|
58
|
+
? getMediaUrl(m.posterKey, publicUrl)
|
|
59
|
+
: null;
|
|
57
60
|
return {
|
|
58
61
|
id: m.id,
|
|
59
62
|
url: mediaUrl,
|
|
60
63
|
previewUrl,
|
|
61
64
|
alt: m.alt,
|
|
62
65
|
blurhash: m.blurhash,
|
|
66
|
+
waveform: m.waveform,
|
|
67
|
+
posterUrl,
|
|
63
68
|
width: m.width,
|
|
64
69
|
height: m.height,
|
|
65
70
|
position: m.position,
|
|
66
71
|
mimeType: m.mimeType,
|
|
72
|
+
originalName: m.originalName,
|
|
73
|
+
size: m.size,
|
|
74
|
+
summary: m.summary,
|
|
75
|
+
chars: m.chars,
|
|
67
76
|
};
|
|
68
77
|
}),
|
|
69
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,7 +49,16 @@ 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;
|
|
@@ -64,6 +79,8 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
64
79
|
siteFooterHtml: navData.siteFooterHtml,
|
|
65
80
|
sidebar,
|
|
66
81
|
uploadMaxFileSize: appConfig.uploadMaxFileSize,
|
|
82
|
+
showComposeDialog,
|
|
83
|
+
showHeader,
|
|
67
84
|
};
|
|
68
85
|
const faviconUrl = appConfig.siteAvatarUrl || undefined;
|
|
69
86
|
const faviconVersion = appConfig.faviconVersion || undefined;
|
|
@@ -78,6 +95,7 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
78
95
|
faviconVersion={faviconVersion}
|
|
79
96
|
noindex={noindex}
|
|
80
97
|
isAuthenticated={navData.isAuthenticated}
|
|
98
|
+
toast={toast}
|
|
81
99
|
>
|
|
82
100
|
<SiteLayout {...layoutProps}>{content}</SiteLayout>
|
|
83
101
|
</BaseLayout>,
|
|
@@ -134,12 +134,16 @@ export function resolveConfig(
|
|
|
134
134
|
imageTransformUrl,
|
|
135
135
|
|
|
136
136
|
// Upload (ENV only)
|
|
137
|
-
uploadMaxFileSize:
|
|
137
|
+
uploadMaxFileSize:
|
|
138
|
+
parseInt(env.UPLOAD_MAX_FILE_SIZE_MB ?? "500", 10) || 500,
|
|
138
139
|
|
|
139
140
|
// Summary extraction (ENV only)
|
|
140
141
|
summaryMaxParagraphs: parseInt(env.SUMMARY_MAX_PARAGRAPHS ?? "5", 10) || 5,
|
|
141
142
|
summaryMaxChars: parseInt(env.SUMMARY_MAX_CHARS ?? "500", 10) || 500,
|
|
142
143
|
|
|
144
|
+
// Slug (ENV only)
|
|
145
|
+
slugIdLength: parseInt(env.SLUG_ID_LENGTH ?? "5", 10) || 5,
|
|
146
|
+
|
|
143
147
|
// Pagination/Feed (ENV only)
|
|
144
148
|
pageSize: parseInt(env.PAGE_SIZE ?? "20", 10) || 20,
|
|
145
149
|
rssFeedLimit: parseInt(env.RSS_FEED_LIMIT ?? "50", 10) || 50,
|
|
@@ -161,7 +165,7 @@ export function resolveConfig(
|
|
|
161
165
|
siteAvatarUrl,
|
|
162
166
|
faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
|
|
163
167
|
|
|
164
|
-
//
|
|
168
|
+
// Settings form placeholders (ENV > Default, without DB)
|
|
165
169
|
fallbacks: {
|
|
166
170
|
siteName: resolveFallback("SITE_NAME", env),
|
|
167
171
|
siteDescription: resolveFallback("SITE_DESCRIPTION", env),
|