@jant/core 0.6.12 → 0.6.14
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/import-site.js +2 -10
- package/bin/commands/migrate-text-attachments.js +1 -3
- package/bin/commands/site/export.js +1 -3
- package/bin/commands/telegram/register-webhooks.js +9 -3
- package/bin/lib/site-snapshot.js +1 -5
- package/dist/{app-D-zAhayc.js → app-2i7-WQHg.js} +426 -450
- package/dist/app-DZDlmh7C.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-S1cdvulk.js → client-BvID5ucz.js} +1 -1
- package/dist/client/_assets/client-Cevfpm8H.css +2 -0
- package/dist/client/_assets/{client-auth-YW92ZH7u.js → client-auth-v3yvhbgZ.js} +596 -590
- package/dist/{export-Ckwh4BiE.js → export-DWn149b7.js} +24 -19
- package/dist/{github-sync-BMmK3JZ0.js → github-sync-C0zyF6XS.js} +2 -2
- package/dist/{github-sync-Dl9wQMar.js → github-sync-Ckk0sJxK.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/export-service.test.ts +1 -2
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/__tests__/import-site-command.test.ts +1 -2
- package/src/client/__tests__/site-header-nav.test.ts +36 -2
- package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
- package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
- package/src/client/components/__tests__/jant-nav-manager.test.ts +107 -22
- package/src/client/components/__tests__/jant-settings-general.test.ts +1 -0
- package/src/client/components/compose-title-from-heading.ts +66 -0
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/inline-image-issues.ts +17 -0
- package/src/client/components/jant-command-palette.ts +100 -70
- package/src/client/components/jant-compose-dialog.ts +3 -1
- package/src/client/components/jant-compose-editor.ts +78 -1
- package/src/client/components/jant-compose-fullscreen.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +245 -155
- package/src/client/components/nav-manager-types.ts +0 -10
- package/src/client/site-header-nav.d.ts +1 -0
- package/src/client/site-header-nav.js +105 -47
- package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
- package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
- package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +148 -1
- package/src/client/tiptap/bubble-menu.ts +3 -3
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/footnotes.ts +156 -0
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- package/src/client/tiptap/markdown-clipboard.ts +7 -2
- package/src/client-auth.ts +0 -1
- package/src/i18n/locales/public/en.po +20 -4
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +20 -4
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +20 -4
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +23 -20
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +23 -20
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +23 -20
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +4 -0
- package/src/lib/__tests__/navigation.test.ts +4 -37
- package/src/lib/__tests__/resolve-config.test.ts +6 -1
- package/src/lib/__tests__/view.test.ts +6 -30
- package/src/lib/github-sync-site-config.ts +0 -2
- package/src/lib/hugo-markdown.ts +1 -1
- package/src/lib/markdown-manager.ts +14 -5
- package/src/lib/navigation.ts +5 -22
- package/src/lib/resolve-config.ts +9 -2
- package/src/lib/site-header-fragment.tsx +37 -0
- package/src/lib/view.ts +2 -7
- package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
- package/src/routes/api/__tests__/nav-items.test.ts +26 -0
- package/src/routes/api/export.ts +0 -2
- package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
- package/src/routes/api/internal/sites.ts +5 -5
- package/src/routes/api/nav-items.ts +25 -4
- package/src/routes/dash/settings.tsx +8 -2
- package/src/routes/feed/__tests__/feed.test.ts +42 -0
- package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
- package/src/routes/feed/feed.ts +1 -0
- package/src/routes/feed/sitemap.ts +2 -6
- package/src/routes/pages/archive.tsx +1 -0
- package/src/routes/pages/collection.tsx +1 -0
- package/src/routes/pages/featured.tsx +0 -4
- package/src/routes/pages/home.tsx +4 -51
- package/src/routes/pages/latest.tsx +2 -51
- package/src/services/__tests__/github-app-installations.test.ts +10 -10
- package/src/services/__tests__/navigation.test.ts +41 -0
- package/src/services/__tests__/post-timeline.test.ts +56 -0
- package/src/services/__tests__/post.test.ts +33 -0
- package/src/services/__tests__/settings.test.ts +5 -20
- package/src/services/export-theme/assets/client-site.js +10 -10
- package/src/services/export-theme/layouts/_default/rss.xml +5 -15
- package/src/services/export-theme/layouts/index.html +26 -39
- package/src/services/export.ts +5 -10
- package/src/services/github-app-installations.ts +1 -3
- package/src/services/navigation.ts +44 -13
- package/src/services/post.ts +80 -57
- package/src/services/settings.ts +0 -10
- package/src/services/site-admin.ts +0 -2
- package/src/services/upload-session.ts +4 -18
- package/src/styles/tokens.css +2 -2
- package/src/styles/ui.css +316 -34
- package/src/types/bindings.ts +0 -1
- package/src/types/config.ts +3 -7
- package/src/ui/compose/ComposeDialog.tsx +28 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +1 -1
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
- package/src/ui/feed/__tests__/timeline-cards.test.ts +8 -3
- package/src/ui/layouts/BaseLayout.tsx +1 -1
- package/src/ui/layouts/SiteLayout.tsx +266 -227
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
- package/dist/app-B5P1b4TF.js +0 -6
- package/dist/client/_assets/client-BUxR-E8O.css +0 -2
- package/src/client/nav-manager-bridge.ts +0 -54
|
@@ -723,7 +723,6 @@ describe("toNavItemView", () => {
|
|
|
723
723
|
const view = toNavItemView(
|
|
724
724
|
makeNavItem({ url: "https://example.com/about" }),
|
|
725
725
|
"/about",
|
|
726
|
-
"latest",
|
|
727
726
|
false,
|
|
728
727
|
"",
|
|
729
728
|
undefined,
|
|
@@ -738,7 +737,6 @@ describe("toNavItemView", () => {
|
|
|
738
737
|
const view = toNavItemView(
|
|
739
738
|
makeNavItem({ url: "https://other.com/about" }),
|
|
740
739
|
"/about",
|
|
741
|
-
"latest",
|
|
742
740
|
false,
|
|
743
741
|
"",
|
|
744
742
|
undefined,
|
|
@@ -755,7 +753,6 @@ describe("toNavItemView", () => {
|
|
|
755
753
|
const view = toNavItemView(
|
|
756
754
|
makeNavItem({ url: "https://jant.example/about" }),
|
|
757
755
|
"/about",
|
|
758
|
-
"latest",
|
|
759
756
|
false,
|
|
760
757
|
"",
|
|
761
758
|
undefined,
|
|
@@ -770,7 +767,6 @@ describe("toNavItemView", () => {
|
|
|
770
767
|
const view = toNavItemView(
|
|
771
768
|
makeNavItem({ url: "https://example.com/blog/about" }),
|
|
772
769
|
"/blog/about",
|
|
773
|
-
"latest",
|
|
774
770
|
false,
|
|
775
771
|
"/blog",
|
|
776
772
|
undefined,
|
|
@@ -799,7 +795,6 @@ describe("toNavItemView", () => {
|
|
|
799
795
|
url: "/settings",
|
|
800
796
|
}),
|
|
801
797
|
"/signin",
|
|
802
|
-
"latest",
|
|
803
798
|
false,
|
|
804
799
|
);
|
|
805
800
|
|
|
@@ -817,7 +812,6 @@ describe("toNavItemView", () => {
|
|
|
817
812
|
url: "/settings",
|
|
818
813
|
}),
|
|
819
814
|
"/settings",
|
|
820
|
-
"latest",
|
|
821
815
|
true,
|
|
822
816
|
);
|
|
823
817
|
|
|
@@ -826,7 +820,7 @@ describe("toNavItemView", () => {
|
|
|
826
820
|
expect(view.isActive).toBe(true);
|
|
827
821
|
});
|
|
828
822
|
|
|
829
|
-
it("resolves the latest built-in item to /
|
|
823
|
+
it("resolves the latest built-in item to /", () => {
|
|
830
824
|
const view = toNavItemView(
|
|
831
825
|
makeNavItem({
|
|
832
826
|
type: "system",
|
|
@@ -835,30 +829,13 @@ describe("toNavItemView", () => {
|
|
|
835
829
|
url: "/latest",
|
|
836
830
|
}),
|
|
837
831
|
"/",
|
|
838
|
-
"latest",
|
|
839
832
|
);
|
|
840
833
|
|
|
841
834
|
expect(view.url).toBe("/");
|
|
842
835
|
expect(view.isActive).toBe(true);
|
|
843
836
|
});
|
|
844
837
|
|
|
845
|
-
it("
|
|
846
|
-
const view = toNavItemView(
|
|
847
|
-
makeNavItem({
|
|
848
|
-
type: "system",
|
|
849
|
-
systemKey: "latest",
|
|
850
|
-
label: "Latest",
|
|
851
|
-
url: "/latest",
|
|
852
|
-
}),
|
|
853
|
-
"/latest",
|
|
854
|
-
"featured",
|
|
855
|
-
);
|
|
856
|
-
|
|
857
|
-
expect(view.url).toBe("/latest");
|
|
858
|
-
expect(view.isActive).toBe(true);
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
it("resolves the featured built-in item to / when it is the home default", () => {
|
|
838
|
+
it("resolves the featured built-in item to /featured", () => {
|
|
862
839
|
const view = toNavItemView(
|
|
863
840
|
makeNavItem({
|
|
864
841
|
type: "system",
|
|
@@ -866,11 +843,10 @@ describe("toNavItemView", () => {
|
|
|
866
843
|
label: "Featured",
|
|
867
844
|
url: "/featured",
|
|
868
845
|
}),
|
|
869
|
-
"/",
|
|
870
|
-
"featured",
|
|
846
|
+
"/featured",
|
|
871
847
|
);
|
|
872
848
|
|
|
873
|
-
expect(view.url).toBe("/");
|
|
849
|
+
expect(view.url).toBe("/featured");
|
|
874
850
|
expect(view.isActive).toBe(true);
|
|
875
851
|
});
|
|
876
852
|
|
|
@@ -903,9 +879,9 @@ describe("toNavItemViews", () => {
|
|
|
903
879
|
makeNavItem({ id: UUID_NAV_2, url: "/archive" }),
|
|
904
880
|
makeNavItem({ id: UUID_NAV_3, url: "https://github.com" }),
|
|
905
881
|
];
|
|
906
|
-
const views = toNavItemViews(items, "/archive"
|
|
882
|
+
const views = toNavItemViews(items, "/archive");
|
|
907
883
|
expect(views).toHaveLength(3);
|
|
908
|
-
expect(views[0]?.url).toBe("/
|
|
884
|
+
expect(views[0]?.url).toBe("/");
|
|
909
885
|
expect(views[0]).toHaveProperty("isActive", false);
|
|
910
886
|
expect(views[1]).toHaveProperty("isActive", true);
|
|
911
887
|
expect(views[2]).toHaveProperty("isExternal", true);
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
|
|
15
15
|
import type { SiteConfig } from "../services/export.js";
|
|
16
16
|
import type { AppVariables } from "../types/app-context.js";
|
|
17
|
-
import { getHomeDefaultViewFromNavItems } from "./navigation.js";
|
|
18
17
|
|
|
19
18
|
export async function buildSyncSiteConfig(c: {
|
|
20
19
|
var: Pick<
|
|
@@ -31,7 +30,6 @@ export async function buildSyncSiteConfig(c: {
|
|
|
31
30
|
siteDescription: appConfig.siteDescription,
|
|
32
31
|
siteLanguage: appConfig.siteLanguage,
|
|
33
32
|
showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
|
|
34
|
-
homeDefaultView: getHomeDefaultViewFromNavItems(navItems),
|
|
35
33
|
mainRssFeed: appConfig.mainRssFeed,
|
|
36
34
|
siteFooter: appConfig.siteFooter,
|
|
37
35
|
showHeaderAvatar: appConfig.showHeaderAvatar,
|
package/src/lib/hugo-markdown.ts
CHANGED
|
@@ -118,7 +118,7 @@ export interface HugoFrontMatter {
|
|
|
118
118
|
collections?: HugoCollectionRef[];
|
|
119
119
|
media?: JantMedia[];
|
|
120
120
|
|
|
121
|
-
// Escape hatch for page-
|
|
121
|
+
// Escape hatch for page-specific export metadata.
|
|
122
122
|
[key: string]: unknown;
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -723,6 +723,7 @@ export function createMarkdownContentExtensions(
|
|
|
723
723
|
heading: { levels: [1, 2, 3] },
|
|
724
724
|
link: { openOnClick: false, autolink: false },
|
|
725
725
|
codeBlock: false,
|
|
726
|
+
trailingNode: { notAfter: ["footnoteDefinition"] },
|
|
726
727
|
}),
|
|
727
728
|
MarkdownCodeBlock,
|
|
728
729
|
Table.configure({
|
|
@@ -742,11 +743,19 @@ export function createMarkdownContentExtensions(
|
|
|
742
743
|
];
|
|
743
744
|
}
|
|
744
745
|
|
|
745
|
-
|
|
746
|
+
/**
|
|
747
|
+
* Normalizes Markdown parser output before it enters the editor schema.
|
|
748
|
+
*
|
|
749
|
+
* @param node - Parsed Tiptap document or descendant node
|
|
750
|
+
* @returns A normalized copy safe to load into an editor
|
|
751
|
+
* @example
|
|
752
|
+
* const normalized = normalizeMarkdownDocument(markdownManager.parse(source));
|
|
753
|
+
*/
|
|
754
|
+
export function normalizeMarkdownDocument(node: JSONContent): JSONContent {
|
|
746
755
|
const normalized: JSONContent = { ...node };
|
|
747
756
|
|
|
748
757
|
if (normalized.content) {
|
|
749
|
-
normalized.content = normalized.content.map(
|
|
758
|
+
normalized.content = normalized.content.map(normalizeMarkdownDocument);
|
|
750
759
|
}
|
|
751
760
|
|
|
752
761
|
if (normalized.marks) {
|
|
@@ -810,9 +819,9 @@ function normalizeMarkdownDoc(node: JSONContent): JSONContent {
|
|
|
810
819
|
child?.type === "text" &&
|
|
811
820
|
typeof child.text === "string" &&
|
|
812
821
|
nextChild?.type === "footnoteReference" &&
|
|
813
|
-
child.text
|
|
822
|
+
/\n[ \t]*$/.test(child.text)
|
|
814
823
|
) {
|
|
815
|
-
const trimmedText = child.text.replace(/\n
|
|
824
|
+
const trimmedText = child.text.replace(/\n[ \t]*$/, "");
|
|
816
825
|
if (trimmedText) {
|
|
817
826
|
nextContent.push({
|
|
818
827
|
...child,
|
|
@@ -854,7 +863,7 @@ export function getMarkdownManager(): MarkdownManager {
|
|
|
854
863
|
}
|
|
855
864
|
|
|
856
865
|
export function parseMarkdownDocument(markdown: string): JSONContent {
|
|
857
|
-
return
|
|
866
|
+
return normalizeMarkdownDocument(getMarkdownManager().parse(markdown));
|
|
858
867
|
}
|
|
859
868
|
|
|
860
869
|
export function serializeMarkdownDocument(doc: JSONContent): string {
|
package/src/lib/navigation.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Context } from "hono";
|
|
8
|
-
import type { Collection,
|
|
8
|
+
import type { Collection, NavItem, NavItemView } from "../types.js";
|
|
9
9
|
import { toNavItemViews } from "./view.js";
|
|
10
10
|
import { render as renderMarkdown, toPlainText } from "./markdown.js";
|
|
11
11
|
|
|
@@ -23,24 +23,11 @@ export interface NavigationData {
|
|
|
23
23
|
siteDescriptionHtml?: string;
|
|
24
24
|
isAuthenticated: boolean;
|
|
25
25
|
collections: Collection[];
|
|
26
|
-
homeDefaultView: FeedKind;
|
|
27
26
|
siteAvatarUrl?: string;
|
|
28
27
|
showHeaderAvatar?: boolean;
|
|
29
28
|
siteFooterHtml?: string;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
export function getHomeDefaultViewFromNavItems(
|
|
33
|
-
items: Pick<NavItem, "type" | "systemKey">[],
|
|
34
|
-
): FeedKind {
|
|
35
|
-
const homeFeedItem = items.find(
|
|
36
|
-
(item) =>
|
|
37
|
-
item.type === "system" &&
|
|
38
|
-
(item.systemKey === "latest" || item.systemKey === "featured"),
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
return homeFeedItem?.systemKey === "featured" ? "featured" : "latest";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
31
|
/**
|
|
45
32
|
* Fetch navigation data for public pages.
|
|
46
33
|
*
|
|
@@ -62,18 +49,16 @@ export function getHomeDefaultViewFromNavItems(
|
|
|
62
49
|
*/
|
|
63
50
|
export async function getNavigationData(
|
|
64
51
|
c: Context,
|
|
65
|
-
options?: { preloadedItems?: NavItem[] },
|
|
52
|
+
options?: { preloadedItems?: NavItem[]; includeCollections?: boolean },
|
|
66
53
|
): Promise<NavigationData> {
|
|
67
|
-
// Callers that already fetched nav items
|
|
68
|
-
//
|
|
69
|
-
// them in to avoid a redundant DB round-trip.
|
|
54
|
+
// Callers that already fetched nav items can pass them in to avoid a
|
|
55
|
+
// redundant DB round-trip.
|
|
70
56
|
const items =
|
|
71
57
|
options?.preloadedItems ?? (await c.var.services.navItems.list());
|
|
72
58
|
const currentPath = c.var.publicPath;
|
|
73
59
|
const appConfig = c.var.appConfig;
|
|
74
60
|
|
|
75
61
|
const siteName = appConfig.siteName;
|
|
76
|
-
const homeDefaultView = getHomeDefaultViewFromNavItems(items);
|
|
77
62
|
const siteFooter = appConfig.siteFooter;
|
|
78
63
|
|
|
79
64
|
// Only include description if explicitly set (DB or env), not the default
|
|
@@ -112,7 +97,6 @@ export async function getNavigationData(
|
|
|
112
97
|
const links = toNavItemViews(
|
|
113
98
|
items,
|
|
114
99
|
currentPath,
|
|
115
|
-
homeDefaultView,
|
|
116
100
|
isAuthenticated,
|
|
117
101
|
appConfig.sitePathPrefix,
|
|
118
102
|
collectionFreshness,
|
|
@@ -120,7 +104,7 @@ export async function getNavigationData(
|
|
|
120
104
|
);
|
|
121
105
|
|
|
122
106
|
// Only load collections when authenticated (for compose dialog)
|
|
123
|
-
if (isAuthenticated) {
|
|
107
|
+
if (isAuthenticated && options?.includeCollections !== false) {
|
|
124
108
|
collections = await c.var.services.collections.listByRecentActivity();
|
|
125
109
|
}
|
|
126
110
|
|
|
@@ -133,7 +117,6 @@ export async function getNavigationData(
|
|
|
133
117
|
siteDescriptionHtml,
|
|
134
118
|
isAuthenticated,
|
|
135
119
|
collections,
|
|
136
|
-
homeDefaultView,
|
|
137
120
|
siteAvatarUrl,
|
|
138
121
|
showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
|
|
139
122
|
siteFooterHtml,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { Bindings } from "../types/bindings.js";
|
|
13
13
|
import type { AppConfig } from "../types/config.js";
|
|
14
14
|
import { CONFIG_FIELDS } from "../types/config.js";
|
|
15
|
+
import type { FeedKind } from "../types/constants.js";
|
|
15
16
|
import { ASSET_BASE_SEGMENT, getPublicAssetBasePath } from "./asset-path.js";
|
|
16
17
|
import {
|
|
17
18
|
getAuthSecret,
|
|
@@ -77,6 +78,10 @@ function parseConfigInt(value: string, fallback: number): number {
|
|
|
77
78
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
function parseFeedKind(value: string, fallback: FeedKind): FeedKind {
|
|
82
|
+
return value === "latest" || value === "featured" ? value : fallback;
|
|
83
|
+
}
|
|
84
|
+
|
|
80
85
|
/**
|
|
81
86
|
* Build a complete AppConfig from environment bindings and DB settings.
|
|
82
87
|
*
|
|
@@ -144,8 +149,10 @@ export function resolveConfig(
|
|
|
144
149
|
siteLanguage: resolve("SITE_LANGUAGE", allSettings, env),
|
|
145
150
|
dashboardLanguage: resolve("DASHBOARD_LANGUAGE", allSettings, env),
|
|
146
151
|
cjkSerifFont: resolve("CJK_SERIF_FONT", allSettings, env),
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
mainRssFeed: parseFeedKind(
|
|
153
|
+
resolve("MAIN_RSS_FEED", allSettings, env),
|
|
154
|
+
"featured",
|
|
155
|
+
),
|
|
149
156
|
timeZone: normalizeTimeZone(resolve("TIME_ZONE", allSettings, env)),
|
|
150
157
|
siteFooter: resolve("SITE_FOOTER", allSettings, env),
|
|
151
158
|
showJantBrandingOnHome:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { Bindings } from "../types.js";
|
|
3
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
4
|
+
import { I18nProvider } from "../i18n/context.js";
|
|
5
|
+
import { getNavigationData } from "./navigation.js";
|
|
6
|
+
import { SiteHeader } from "../ui/layouts/SiteLayout.js";
|
|
7
|
+
|
|
8
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Render the current public site header as a replaceable HTML fragment.
|
|
12
|
+
*
|
|
13
|
+
* @param c - Current Hono request context
|
|
14
|
+
* @returns Server-rendered header, drawer backdrop, and mobile drawer HTML
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const headerHtml = await renderSiteHeaderHtml(c);
|
|
19
|
+
* return c.json({ headerHtml });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export async function renderSiteHeaderHtml(c: Context<Env>): Promise<string> {
|
|
23
|
+
const navData = await getNavigationData(c, { includeCollections: false });
|
|
24
|
+
|
|
25
|
+
return String(
|
|
26
|
+
<I18nProvider c={c}>
|
|
27
|
+
<SiteHeader
|
|
28
|
+
siteName={navData.siteName}
|
|
29
|
+
links={navData.links}
|
|
30
|
+
currentPath={navData.currentPath}
|
|
31
|
+
sitePathPrefix={navData.sitePathPrefix}
|
|
32
|
+
siteAvatarUrl={navData.siteAvatarUrl}
|
|
33
|
+
showHeaderAvatar={navData.showHeaderAvatar}
|
|
34
|
+
/>
|
|
35
|
+
</I18nProvider>,
|
|
36
|
+
);
|
|
37
|
+
}
|
package/src/lib/view.ts
CHANGED
|
@@ -23,7 +23,6 @@ import type {
|
|
|
23
23
|
NavItemType,
|
|
24
24
|
NavItemPlacement,
|
|
25
25
|
AppConfig,
|
|
26
|
-
FeedKind,
|
|
27
26
|
SystemNavKey,
|
|
28
27
|
} from "../types.js";
|
|
29
28
|
import { SYSTEM_NAV_KEYS } from "../types/constants.js";
|
|
@@ -474,7 +473,6 @@ export function toPostViewsFromPosts(
|
|
|
474
473
|
export function toNavItemView(
|
|
475
474
|
item: NavItem,
|
|
476
475
|
currentPath: string,
|
|
477
|
-
homeDefaultView: FeedKind = "latest",
|
|
478
476
|
isAuthenticated = false,
|
|
479
477
|
sitePathPrefix = "",
|
|
480
478
|
collectionFreshness?: Map<string, number>,
|
|
@@ -491,13 +489,12 @@ export function toNavItemView(
|
|
|
491
489
|
url = config.url;
|
|
492
490
|
}
|
|
493
491
|
|
|
494
|
-
// Special overrides for conditional system URLs
|
|
495
492
|
if (item.systemKey === "latest") {
|
|
496
|
-
url =
|
|
493
|
+
url = "/";
|
|
497
494
|
}
|
|
498
495
|
|
|
499
496
|
if (item.systemKey === "featured") {
|
|
500
|
-
url =
|
|
497
|
+
url = "/featured";
|
|
501
498
|
}
|
|
502
499
|
|
|
503
500
|
if (item.systemKey === "settings") {
|
|
@@ -557,7 +554,6 @@ export function toNavItemView(
|
|
|
557
554
|
export function toNavItemViews(
|
|
558
555
|
items: NavItem[],
|
|
559
556
|
currentPath: string,
|
|
560
|
-
homeDefaultView: FeedKind = "latest",
|
|
561
557
|
isAuthenticated = false,
|
|
562
558
|
sitePathPrefix = "",
|
|
563
559
|
collectionFreshness?: Map<string, number>,
|
|
@@ -567,7 +563,6 @@ export function toNavItemViews(
|
|
|
567
563
|
toNavItemView(
|
|
568
564
|
item,
|
|
569
565
|
currentPath,
|
|
570
|
-
homeDefaultView,
|
|
571
566
|
isAuthenticated,
|
|
572
567
|
sitePathPrefix,
|
|
573
568
|
collectionFreshness,
|
|
@@ -32,32 +32,41 @@ const REMAPPED_MEDIA_KEY = `media/${REMAPPED_TARGET_SITE_ID}/files/${SNAPSHOT_ME
|
|
|
32
32
|
const REMAPPED_POSTER_KEY = `media/${REMAPPED_TARGET_SITE_ID}/posters/${SNAPSHOT_MEDIA_ID}.webp`;
|
|
33
33
|
const REMAPPED_AVATAR_KEY = `media/${REMAPPED_TARGET_SITE_ID}/assets/avatar/${SNAPSHOT_AVATAR_MEDIA_ID}.png`;
|
|
34
34
|
const REMAPPED_APPLE_TOUCH_KEY = `media/${REMAPPED_TARGET_SITE_ID}/assets/favicon/apple-touch-icon.png`;
|
|
35
|
+
const NODE_CLI_ENV_KEYS = [
|
|
36
|
+
"DATABASE_URL",
|
|
37
|
+
"DATA_DIR",
|
|
38
|
+
"LOCAL_STORAGE_PATH",
|
|
39
|
+
"STORAGE_DRIVER",
|
|
40
|
+
"S3_ENDPOINT",
|
|
41
|
+
"S3_BUCKET",
|
|
42
|
+
"S3_REGION",
|
|
43
|
+
"S3_PUBLIC_URL",
|
|
44
|
+
"S3_ACCESS_KEY_ID",
|
|
45
|
+
"S3_SECRET_ACCESS_KEY",
|
|
46
|
+
"SITE_RESOLUTION_MODE",
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
function useLocalSnapshotRuntime(databaseUrl: string, storagePath: string) {
|
|
50
|
+
delete process.env.DATA_DIR;
|
|
51
|
+
process.env.DATABASE_URL = databaseUrl;
|
|
52
|
+
process.env.LOCAL_STORAGE_PATH = storagePath;
|
|
53
|
+
process.env.STORAGE_DRIVER = "local";
|
|
54
|
+
}
|
|
35
55
|
|
|
36
56
|
describe("jant site snapshot export/import", () => {
|
|
37
57
|
const tempDirs: string[] = [];
|
|
38
|
-
const originalEnv =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
SITE_RESOLUTION_MODE: process.env.SITE_RESOLUTION_MODE,
|
|
42
|
-
};
|
|
58
|
+
const originalEnv = Object.fromEntries(
|
|
59
|
+
NODE_CLI_ENV_KEYS.map((key) => [key, process.env[key]]),
|
|
60
|
+
) as Record<(typeof NODE_CLI_ENV_KEYS)[number], string | undefined>;
|
|
43
61
|
|
|
44
62
|
afterEach(async () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
delete process.env.LOCAL_STORAGE_PATH;
|
|
53
|
-
} else {
|
|
54
|
-
process.env.LOCAL_STORAGE_PATH = originalEnv.LOCAL_STORAGE_PATH;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (originalEnv.SITE_RESOLUTION_MODE === undefined) {
|
|
58
|
-
delete process.env.SITE_RESOLUTION_MODE;
|
|
59
|
-
} else {
|
|
60
|
-
process.env.SITE_RESOLUTION_MODE = originalEnv.SITE_RESOLUTION_MODE;
|
|
63
|
+
for (const key of NODE_CLI_ENV_KEYS) {
|
|
64
|
+
const value = originalEnv[key];
|
|
65
|
+
if (value === undefined) {
|
|
66
|
+
delete process.env[key];
|
|
67
|
+
} else {
|
|
68
|
+
process.env[key] = value;
|
|
69
|
+
}
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
await Promise.all(
|
|
@@ -219,8 +228,7 @@ describe("jant site snapshot export/import", () => {
|
|
|
219
228
|
contentType: "image/png",
|
|
220
229
|
});
|
|
221
230
|
|
|
222
|
-
|
|
223
|
-
process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
|
|
231
|
+
useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
|
|
224
232
|
|
|
225
233
|
const exportLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
226
234
|
const { run: runExport } =
|
|
@@ -241,8 +249,7 @@ describe("jant site snapshot export/import", () => {
|
|
|
241
249
|
`Exported Node database snapshot to ${snapshotPath}`,
|
|
242
250
|
);
|
|
243
251
|
|
|
244
|
-
|
|
245
|
-
process.env.LOCAL_STORAGE_PATH = targetStoragePath;
|
|
252
|
+
useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
|
|
246
253
|
|
|
247
254
|
const importLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
248
255
|
const { run: runImport } =
|
|
@@ -379,8 +386,7 @@ describe("jant site snapshot export/import", () => {
|
|
|
379
386
|
sourceSqlite.close();
|
|
380
387
|
}
|
|
381
388
|
|
|
382
|
-
|
|
383
|
-
process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
|
|
389
|
+
useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
|
|
384
390
|
|
|
385
391
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
386
392
|
const { run: runExport } =
|
|
@@ -453,16 +459,14 @@ describe("jant site snapshot export/import", () => {
|
|
|
453
459
|
targetSqlite.close();
|
|
454
460
|
}
|
|
455
461
|
|
|
456
|
-
|
|
457
|
-
process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
|
|
462
|
+
useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
|
|
458
463
|
|
|
459
464
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
460
465
|
const { run: runExport } =
|
|
461
466
|
await import("../../../bin/commands/site/snapshot/export.js");
|
|
462
467
|
await runExport(["--output", snapshotPath, "--skip-objects"]);
|
|
463
468
|
|
|
464
|
-
|
|
465
|
-
process.env.LOCAL_STORAGE_PATH = targetStoragePath;
|
|
469
|
+
useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
|
|
466
470
|
|
|
467
471
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
468
472
|
const { run: runImport } =
|
|
@@ -538,16 +542,14 @@ describe("jant site snapshot export/import", () => {
|
|
|
538
542
|
targetSqlite.close();
|
|
539
543
|
}
|
|
540
544
|
|
|
541
|
-
|
|
542
|
-
process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
|
|
545
|
+
useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
|
|
543
546
|
|
|
544
547
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
545
548
|
const { run: runExport } =
|
|
546
549
|
await import("../../../bin/commands/site/snapshot/export.js");
|
|
547
550
|
await runExport(["--output", snapshotPath, "--skip-objects"]);
|
|
548
551
|
|
|
549
|
-
|
|
550
|
-
process.env.LOCAL_STORAGE_PATH = targetStoragePath;
|
|
552
|
+
useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
|
|
551
553
|
|
|
552
554
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
553
555
|
const { run: runImport } =
|
|
@@ -580,8 +582,10 @@ describe("jant site snapshot export/import", () => {
|
|
|
580
582
|
tempDirs.push(root);
|
|
581
583
|
|
|
582
584
|
const snapshotPath = join(root, "snapshot");
|
|
583
|
-
|
|
584
|
-
|
|
585
|
+
useLocalSnapshotRuntime(
|
|
586
|
+
`file:${join(root, "jant.sqlite")}`,
|
|
587
|
+
join(root, "media"),
|
|
588
|
+
);
|
|
585
589
|
|
|
586
590
|
await rm(snapshotPath, { recursive: true, force: true });
|
|
587
591
|
await mkdir(snapshotPath, { recursive: true });
|
|
@@ -702,15 +706,13 @@ describe("jant site snapshot export/import", () => {
|
|
|
702
706
|
targetSqlite.close();
|
|
703
707
|
}
|
|
704
708
|
|
|
705
|
-
|
|
706
|
-
process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
|
|
709
|
+
useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
|
|
707
710
|
|
|
708
711
|
const { run: runExport } =
|
|
709
712
|
await import("../../../bin/commands/site/snapshot/export.js");
|
|
710
713
|
await runExport(["--output", snapshotPath]);
|
|
711
714
|
|
|
712
|
-
|
|
713
|
-
process.env.LOCAL_STORAGE_PATH = targetStoragePath;
|
|
715
|
+
useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
|
|
714
716
|
|
|
715
717
|
const { run: runImport } =
|
|
716
718
|
await import("../../../bin/commands/site/snapshot/import.js");
|
|
@@ -868,16 +870,14 @@ describe("jant site snapshot export/import", () => {
|
|
|
868
870
|
targetSqlite.close();
|
|
869
871
|
}
|
|
870
872
|
|
|
871
|
-
|
|
872
|
-
process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
|
|
873
|
+
useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
|
|
873
874
|
delete process.env.SITE_RESOLUTION_MODE;
|
|
874
875
|
|
|
875
876
|
const { run: runExport } =
|
|
876
877
|
await import("../../../bin/commands/site/snapshot/export.js");
|
|
877
878
|
await runExport(["--output", snapshotPath]);
|
|
878
879
|
|
|
879
|
-
|
|
880
|
-
process.env.LOCAL_STORAGE_PATH = targetStoragePath;
|
|
880
|
+
useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
|
|
881
881
|
delete process.env.SITE_RESOLUTION_MODE;
|
|
882
882
|
|
|
883
883
|
const importLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
@@ -77,6 +77,32 @@ describe("Nav Items API Routes", () => {
|
|
|
77
77
|
expect(body.label).toBe("GitHub");
|
|
78
78
|
expect(body.url).toBe("https://github.com");
|
|
79
79
|
expect(body.type).toBe("link");
|
|
80
|
+
expect(body.headerHtml).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("includes a site header fragment for navigation editor requests", async () => {
|
|
84
|
+
const { app } = createTestApp({ authenticated: true });
|
|
85
|
+
app.route("/api/nav-items", navItemsApiRoutes);
|
|
86
|
+
|
|
87
|
+
const res = await app.request("/api/nav-items", {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
"X-Jant-Site-Header": "include",
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
type: "link",
|
|
95
|
+
label: "Docs",
|
|
96
|
+
url: "/docs",
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(res.status).toBe(201);
|
|
101
|
+
const body = await res.json();
|
|
102
|
+
expect(body.label).toBe("Docs");
|
|
103
|
+
expect(body.headerHtml).toContain('data-site-header-fragment="header"');
|
|
104
|
+
expect(body.headerHtml).toContain('data-site-header-fragment="drawer"');
|
|
105
|
+
expect(body.headerHtml).toContain("Docs");
|
|
80
106
|
});
|
|
81
107
|
|
|
82
108
|
it("creates a system nav item when authenticated", async () => {
|
package/src/routes/api/export.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../types/app-context.js";
|
|
8
|
-
import { getHomeDefaultViewFromNavItems } from "../../lib/navigation.js";
|
|
9
8
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
10
9
|
import { createExportService } from "../../services/export.js";
|
|
11
10
|
|
|
@@ -25,7 +24,6 @@ exportApiRoutes.post("/hugo", requireAuthApi(), async (c) => {
|
|
|
25
24
|
siteDescription: appConfig.siteDescription,
|
|
26
25
|
siteLanguage: appConfig.siteLanguage,
|
|
27
26
|
showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
|
|
28
|
-
homeDefaultView: getHomeDefaultViewFromNavItems(navItems),
|
|
29
27
|
mainRssFeed: appConfig.mainRssFeed,
|
|
30
28
|
siteFooter: appConfig.siteFooter,
|
|
31
29
|
showHeaderAvatar: appConfig.showHeaderAvatar,
|
|
@@ -136,7 +136,7 @@ describe("Internal upload admin routes", () => {
|
|
|
136
136
|
expect(storage.files.has(row.tempStorageKey)).toBe(false);
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
it("
|
|
139
|
+
it("keeps finalized unattached media during upload cleanup", async () => {
|
|
140
140
|
const storage = createMockStorage();
|
|
141
141
|
const { app, services, sqlite } = createTestApp({
|
|
142
142
|
authenticated: false,
|
|
@@ -187,28 +187,24 @@ describe("Internal upload admin routes", () => {
|
|
|
187
187
|
await expect(res.json()).resolves.toEqual({
|
|
188
188
|
abortedMultipartUploads: 0,
|
|
189
189
|
deletedSessions: 0,
|
|
190
|
-
deletedOrphanMedia:
|
|
191
|
-
// The reaped orphan's object is freshly enqueued (30 days out), so the
|
|
192
|
-
// same sweep purges nothing yet.
|
|
190
|
+
deletedOrphanMedia: 0,
|
|
193
191
|
purgedStorageObjects: 0,
|
|
194
192
|
});
|
|
195
193
|
|
|
196
|
-
//
|
|
197
|
-
//
|
|
194
|
+
// Finalized media may be referenced from post body JSON without being a
|
|
195
|
+
// post attachment, so upload cleanup must not delete it from `postId IS
|
|
196
|
+
// NULL` alone.
|
|
198
197
|
expect(
|
|
199
198
|
sqlite.prepare("select id from media where id = ?").get(oldOrphan.id),
|
|
200
|
-
).
|
|
201
|
-
expect(storage.files.has(oldStorageKey)).toBe(
|
|
202
|
-
expect([...storage.files.keys()].some((k) => k.startsWith("trash/"))).toBe(
|
|
203
|
-
true,
|
|
204
|
-
);
|
|
199
|
+
).toBeDefined();
|
|
200
|
+
expect(storage.files.has(oldStorageKey)).toBe(true);
|
|
205
201
|
expect(
|
|
206
202
|
sqlite
|
|
207
203
|
.prepare("select id from storage_purge where original_key = ?")
|
|
208
204
|
.get(oldStorageKey),
|
|
209
|
-
).
|
|
205
|
+
).toBeUndefined();
|
|
210
206
|
|
|
211
|
-
// Fresh
|
|
207
|
+
// Fresh unattached media is also untouched.
|
|
212
208
|
expect(
|
|
213
209
|
sqlite.prepare("select id from media where id = ?").get(freshOrphan.id),
|
|
214
210
|
).toBeDefined();
|