@jant/core 0.6.12 → 0.6.13
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-CWJFPjuR.js +6 -0
- package/dist/{app-D-zAhayc.js → app-HnzmsaqU.js} +416 -444
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-S1cdvulk.js → client-3_OaxhTs.js} +1 -1
- package/dist/client/_assets/{client-BUxR-E8O.css → client-DCrcHVJl.css} +1 -1
- package/dist/client/_assets/{client-auth-YW92ZH7u.js → client-auth-TsYq90xm.js} +570 -567
- package/dist/{export-Ckwh4BiE.js → export-Dfbq9aot.js} +10 -13
- package/dist/{github-sync-Dl9wQMar.js → github-sync--PSoE-Ne.js} +1 -1
- package/dist/{github-sync-BMmK3JZ0.js → github-sync-BGTLx8Mf.js} +2 -2
- 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-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-compose-dialog.ts +2 -0
- 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__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- 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__/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/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 +200 -6
- 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/layouts/SiteLayout.tsx +249 -216
- package/dist/app-B5P1b4TF.js +0 -6
- package/src/client/nav-manager-bridge.ts +0 -54
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-
|
|
1
|
+
import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-Dfbq9aot.js";
|
|
2
2
|
import { r as getInstallationToken } from "./github-app-BbklkFmU.js";
|
|
3
3
|
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-BgSiE71w.js";
|
|
4
4
|
//#region src/lib/markdown-to-tiptap.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./url-BMYO-Zlt.js";
|
|
2
|
-
import "./export-
|
|
3
|
-
import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync-
|
|
2
|
+
import "./export-Dfbq9aot.js";
|
|
3
|
+
import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync--PSoE-Ne.js";
|
|
4
4
|
export { classifyRepoForSync, createGitHubSyncService };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { y as url_exports } from "./url-BMYO-Zlt.js";
|
|
2
|
-
import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, j as MAX_PINNED_POSTS, k as FORMATS, m as defaultFeedRenderer, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-
|
|
3
|
-
import { S as time_exports, a as markdown_exports } from "./export-
|
|
2
|
+
import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, j as MAX_PINNED_POSTS, k as FORMATS, m as defaultFeedRenderer, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-HnzmsaqU.js";
|
|
3
|
+
import { S as time_exports, a as markdown_exports } from "./export-Dfbq9aot.js";
|
|
4
4
|
import "./env-OHRKGcMj.js";
|
|
5
|
-
import "./github-sync-
|
|
5
|
+
import "./github-sync--PSoE-Ne.js";
|
|
6
6
|
export { FORMATS, MAX_MEDIA_ATTACHMENTS, MAX_PINNED_POSTS, MEDIA_KINDS, NAV_ITEM_TYPES, SORT_ORDERS, STATUSES, TEXT_ATTACHMENT_CONTENT_FORMATS, createApp, createMediaContext, defaultFeedRenderer, markdown_exports as markdown, time_exports as time, toArchiveGroups, toArchiveGroupsWithMedia, toMediaView, toNavItemView, toNavItemViews, toPostView, toPostViews, toSearchResultView, url_exports as url };
|
package/dist/node.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "./url-BMYO-Zlt.js";
|
|
2
|
-
import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as getWebhookUrl, d as BUILTIN_FONT_THEMES, f as getCjkSerifCssVariables, g as pgSchemaBundle, h as createStorageDriver, i as createSiteService, l as setMyCommands, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getFontThemeCssVariables, r as createNodeRequestRuntime, s as resolveConfig, t as createApp, u as setWebhook, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-
|
|
3
|
-
import { t as createExportService } from "./export-
|
|
2
|
+
import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as getWebhookUrl, d as BUILTIN_FONT_THEMES, f as getCjkSerifCssVariables, g as pgSchemaBundle, h as createStorageDriver, i as createSiteService, l as setMyCommands, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getFontThemeCssVariables, r as createNodeRequestRuntime, s as resolveConfig, t as createApp, u as setWebhook, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-HnzmsaqU.js";
|
|
3
|
+
import { t as createExportService } from "./export-Dfbq9aot.js";
|
|
4
4
|
import { C as shouldTrustProxy, S as getTelegramWebhookSecret, b as getSiteResolutionMode, d as getHostedControlPlaneBaseUrl, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as getTelegramBotPool, y as getPort } from "./env-OHRKGcMj.js";
|
|
5
|
-
import "./github-sync-
|
|
5
|
+
import "./github-sync--PSoE-Ne.js";
|
|
6
6
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
7
7
|
import { serve } from "@hono/node-server";
|
|
8
8
|
import Database from "better-sqlite3";
|
|
@@ -529,7 +529,7 @@ async function createNodeRequestHandler(options) {
|
|
|
529
529
|
async function start(env = process.env, app) {
|
|
530
530
|
const handler = await createNodeRequestHandler({
|
|
531
531
|
env,
|
|
532
|
-
app: async () => app ?? (await import("./app-
|
|
532
|
+
app: async () => app ?? (await import("./app-CWJFPjuR.js")).createApp()
|
|
533
533
|
});
|
|
534
534
|
const hostname = resolveHost(env);
|
|
535
535
|
const port = resolvePort(env);
|
package/package.json
CHANGED
|
@@ -281,7 +281,6 @@ describe("createExportService (Hugo)", () => {
|
|
|
281
281
|
makeSiteConfig({
|
|
282
282
|
siteName: "My Site",
|
|
283
283
|
siteUrl: "https://my.example",
|
|
284
|
-
homeDefaultView: "featured",
|
|
285
284
|
}),
|
|
286
285
|
);
|
|
287
286
|
const files = filesToMap(await service.generateHugoFiles());
|
|
@@ -292,7 +291,7 @@ describe("createExportService (Hugo)", () => {
|
|
|
292
291
|
expect(toml).toContain('theme = "jant"');
|
|
293
292
|
expect(toml).toMatch(/\[permalinks\][\s\S]*post = "\/:slug\/"/);
|
|
294
293
|
expect(toml).toContain("[params]");
|
|
295
|
-
expect(toml).toContain(
|
|
294
|
+
expect(toml).not.toContain("home_default_view");
|
|
296
295
|
});
|
|
297
296
|
|
|
298
297
|
it("configures hugo.toml for Atom RSS output with per-section opt-in", async () => {
|
|
@@ -123,6 +123,9 @@ export function createTestApp(options: TestAppOptions = {}) {
|
|
|
123
123
|
c.set("appConfig", resolveConfig(c.env, allSettings));
|
|
124
124
|
c.set("storage", options.storage ?? null);
|
|
125
125
|
c.set("rateLimiter", rateLimiter);
|
|
126
|
+
const publicRequestUrl = c.req.url;
|
|
127
|
+
c.set("publicRequestUrl", publicRequestUrl);
|
|
128
|
+
c.set("publicPath", new URL(publicRequestUrl).pathname);
|
|
126
129
|
|
|
127
130
|
// i18n (English default for tests)
|
|
128
131
|
const i18n = createI18n("en");
|
|
@@ -88,7 +88,6 @@ export function makeSiteConfig(over: Partial<SiteConfig> = {}): SiteConfig {
|
|
|
88
88
|
siteDescription: "Export fixture",
|
|
89
89
|
siteLanguage: "en",
|
|
90
90
|
showJantBrandingOnHome: true,
|
|
91
|
-
homeDefaultView: "latest",
|
|
92
91
|
mainRssFeed: "latest",
|
|
93
92
|
siteFooter: "",
|
|
94
93
|
showHeaderAvatar: false,
|
|
@@ -98,7 +98,6 @@ describe("Hugo import CLI helpers", () => {
|
|
|
98
98
|
'site_name = "Example Site"',
|
|
99
99
|
'site_description = "A description"',
|
|
100
100
|
'site_language = "en"',
|
|
101
|
-
'home_default_view = "featured"',
|
|
102
101
|
"show_jant_branding_on_home = true",
|
|
103
102
|
"show_header_avatar = false",
|
|
104
103
|
"noindex = false",
|
|
@@ -136,7 +135,7 @@ describe("Hugo import CLI helpers", () => {
|
|
|
136
135
|
expect(siteConfig.title).toBe("Example Site");
|
|
137
136
|
expect(siteConfig.base_url).toBe("https://example.com/");
|
|
138
137
|
expect(siteConfig.extra.jant.theme_id).toBe("paper");
|
|
139
|
-
expect(siteConfig.extra.jant
|
|
138
|
+
expect(siteConfig.extra.jant).not.toHaveProperty("home_default_view");
|
|
140
139
|
expect(siteConfig.extra.jant.nav_exported).toBe(true);
|
|
141
140
|
expect(siteConfig.extra.jant.nav).toHaveLength(1);
|
|
142
141
|
expect(siteConfig.extra.jant.collections_directory_exported).toBe(true);
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
5
|
type SiteHeaderInit = typeof import("../site-header-nav.js").initSiteHeaderNav;
|
|
6
|
+
type SiteHeaderDestroy =
|
|
7
|
+
typeof import("../site-header-nav.js").destroySiteHeaderNav;
|
|
6
8
|
|
|
7
9
|
function createDrawerDOM(): HTMLElement {
|
|
8
10
|
const root = document.createElement("div");
|
|
@@ -50,13 +52,15 @@ function createMoreDropdownDOM(): HTMLElement {
|
|
|
50
52
|
|
|
51
53
|
describe("site header more dropdown", () => {
|
|
52
54
|
let initSiteHeaderNav: SiteHeaderInit;
|
|
55
|
+
let destroySiteHeaderNav: SiteHeaderDestroy;
|
|
53
56
|
let root: HTMLElement;
|
|
54
57
|
|
|
55
58
|
beforeEach(async () => {
|
|
56
59
|
document.body.innerHTML = "";
|
|
57
60
|
vi.resetModules();
|
|
58
61
|
root = createMoreDropdownDOM();
|
|
59
|
-
({ initSiteHeaderNav } =
|
|
62
|
+
({ initSiteHeaderNav, destroySiteHeaderNav } =
|
|
63
|
+
await import("../site-header-nav.js"));
|
|
60
64
|
initSiteHeaderNav(root);
|
|
61
65
|
});
|
|
62
66
|
|
|
@@ -130,10 +134,26 @@ describe("site header more dropdown", () => {
|
|
|
130
134
|
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
131
135
|
expect(popover.getAttribute("aria-hidden")).toBe("true");
|
|
132
136
|
});
|
|
137
|
+
|
|
138
|
+
it("removes dropdown listeners when destroyed", () => {
|
|
139
|
+
const trigger = root.querySelector(
|
|
140
|
+
".site-header-more-btn",
|
|
141
|
+
) as HTMLButtonElement;
|
|
142
|
+
const popover = root.querySelector(
|
|
143
|
+
".site-header-more-popover",
|
|
144
|
+
) as HTMLElement;
|
|
145
|
+
|
|
146
|
+
destroySiteHeaderNav(root);
|
|
147
|
+
trigger.click();
|
|
148
|
+
|
|
149
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
150
|
+
expect(popover.getAttribute("aria-hidden")).toBe("true");
|
|
151
|
+
});
|
|
133
152
|
});
|
|
134
153
|
|
|
135
154
|
describe("site header nav drawer", () => {
|
|
136
155
|
let initSiteHeaderNav: SiteHeaderInit;
|
|
156
|
+
let destroySiteHeaderNav: SiteHeaderDestroy;
|
|
137
157
|
let root: HTMLElement;
|
|
138
158
|
|
|
139
159
|
beforeEach(async () => {
|
|
@@ -141,7 +161,8 @@ describe("site header nav drawer", () => {
|
|
|
141
161
|
document.documentElement.classList.remove("drawer-open");
|
|
142
162
|
vi.resetModules();
|
|
143
163
|
root = createDrawerDOM();
|
|
144
|
-
({ initSiteHeaderNav } =
|
|
164
|
+
({ initSiteHeaderNav, destroySiteHeaderNav } =
|
|
165
|
+
await import("../site-header-nav.js"));
|
|
145
166
|
initSiteHeaderNav(root);
|
|
146
167
|
});
|
|
147
168
|
|
|
@@ -247,4 +268,17 @@ describe("site header nav drawer", () => {
|
|
|
247
268
|
expect(drawer.getAttribute("aria-hidden")).toBe("true");
|
|
248
269
|
expect(hamburger.getAttribute("aria-expanded")).toBe("false");
|
|
249
270
|
});
|
|
271
|
+
|
|
272
|
+
it("removes drawer listeners when destroyed", () => {
|
|
273
|
+
const hamburger = root.querySelector(
|
|
274
|
+
".site-header-hamburger",
|
|
275
|
+
) as HTMLButtonElement;
|
|
276
|
+
const drawer = root.querySelector("#site-nav-drawer") as HTMLElement;
|
|
277
|
+
|
|
278
|
+
destroySiteHeaderNav(root);
|
|
279
|
+
hamburger.click();
|
|
280
|
+
|
|
281
|
+
expect(drawer.getAttribute("aria-hidden")).toBe("true");
|
|
282
|
+
expect(hamburger.getAttribute("aria-expanded")).toBe("false");
|
|
283
|
+
});
|
|
250
284
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { JSONContent } from "@tiptap/core";
|
|
3
|
+
|
|
4
|
+
import { promoteLeadingH1Title } from "../compose-title-from-heading.js";
|
|
5
|
+
|
|
6
|
+
function doc(...content: JSONContent[]): JSONContent {
|
|
7
|
+
return { type: "doc", content };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function paragraph(text = ""): JSONContent {
|
|
11
|
+
return text
|
|
12
|
+
? { type: "paragraph", content: [{ type: "text", text }] }
|
|
13
|
+
: { type: "paragraph" };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function heading(level: number, text = ""): JSONContent {
|
|
17
|
+
return text
|
|
18
|
+
? {
|
|
19
|
+
type: "heading",
|
|
20
|
+
attrs: { level },
|
|
21
|
+
content: [{ type: "text", text }],
|
|
22
|
+
}
|
|
23
|
+
: { type: "heading", attrs: { level } };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("promoteLeadingH1Title", () => {
|
|
27
|
+
it("extracts the first non-empty H1 and removes it from the body", () => {
|
|
28
|
+
const result = promoteLeadingH1Title(
|
|
29
|
+
doc(paragraph(), heading(1, " My Title "), paragraph("Body")),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(result?.title).toBe("My Title");
|
|
33
|
+
expect(result?.headingIndex).toBe(1);
|
|
34
|
+
expect(result?.bodyJson).toEqual(doc(paragraph("Body")));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns null when the first non-empty block is not an H1", () => {
|
|
38
|
+
const result = promoteLeadingH1Title(
|
|
39
|
+
doc(paragraph("Intro"), heading(1, "Title")),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(result).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns null for empty H1 blocks", () => {
|
|
46
|
+
const result = promoteLeadingH1Title(doc(heading(1), paragraph("Body")));
|
|
47
|
+
|
|
48
|
+
expect(result).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns a null body when the promoted H1 is the only content", () => {
|
|
52
|
+
const result = promoteLeadingH1Title(doc(heading(1, "Only title")));
|
|
53
|
+
|
|
54
|
+
expect(result?.title).toBe("Only title");
|
|
55
|
+
expect(result?.bodyJson).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -4,7 +4,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
4
4
|
vi.mock("../../upload-with-metadata.js", () => ({
|
|
5
5
|
uploadWithMetadata: vi.fn(),
|
|
6
6
|
}));
|
|
7
|
-
import type { Editor } from "@tiptap/core";
|
|
7
|
+
import type { Editor, JSONContent } from "@tiptap/core";
|
|
8
8
|
import type { Slice } from "@tiptap/pm/model";
|
|
9
9
|
import * as ProseMirrorView from "@tiptap/pm/view";
|
|
10
10
|
import type { ComposeLabels } from "../compose-types.js";
|
|
@@ -301,6 +301,25 @@ async function createElement(
|
|
|
301
301
|
return el;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
function tiptapDoc(...content: JSONContent[]): JSONContent {
|
|
305
|
+
return { type: "doc", content };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function tiptapHeading(level: number, text: string): JSONContent {
|
|
309
|
+
return {
|
|
310
|
+
type: "heading",
|
|
311
|
+
attrs: { level },
|
|
312
|
+
content: [{ type: "text", text }],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function tiptapParagraph(text: string): JSONContent {
|
|
317
|
+
return {
|
|
318
|
+
type: "paragraph",
|
|
319
|
+
content: [{ type: "text", text }],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
304
323
|
async function toggleEmojiPicker(el: JantComposeEditor) {
|
|
305
324
|
(
|
|
306
325
|
el as unknown as {
|
|
@@ -905,6 +924,77 @@ describe("JantComposeEditor", () => {
|
|
|
905
924
|
expect(el.getData().title).toBe("My Title");
|
|
906
925
|
});
|
|
907
926
|
|
|
927
|
+
it("promotes a leading H1 to the note title", async () => {
|
|
928
|
+
const el = await createElement("note");
|
|
929
|
+
const editor = requireEditor(el);
|
|
930
|
+
editor.commands.setContent(
|
|
931
|
+
tiptapDoc(tiptapHeading(1, "My Markdown Title"), tiptapParagraph("Body")),
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
el.promoteLeadingH1Title({ force: true });
|
|
935
|
+
await el.updateComplete;
|
|
936
|
+
|
|
937
|
+
const data = el.getData();
|
|
938
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
939
|
+
|
|
940
|
+
expect(data.title).toBe("My Markdown Title");
|
|
941
|
+
expect(el._showTitle).toBe(true);
|
|
942
|
+
expect(body.content?.[0]).toEqual(tiptapParagraph("Body"));
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it("keeps a leading H1 in the body while the cursor is still in it", async () => {
|
|
946
|
+
const el = await createElement("note");
|
|
947
|
+
const editor = requireEditor(el);
|
|
948
|
+
|
|
949
|
+
editor.commands.setContent(tiptapDoc(tiptapHeading(1, "Draft title")));
|
|
950
|
+
await el.updateComplete;
|
|
951
|
+
|
|
952
|
+
const data = el.getData();
|
|
953
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
954
|
+
|
|
955
|
+
expect(data.title).toBe("");
|
|
956
|
+
expect(el._showTitle).toBe(false);
|
|
957
|
+
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Draft title"));
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it("does not overwrite an explicit note title with a leading H1", async () => {
|
|
961
|
+
const el = await createElement("note");
|
|
962
|
+
const editor = requireEditor(el);
|
|
963
|
+
el._title = "Manual title";
|
|
964
|
+
el._showTitle = true;
|
|
965
|
+
|
|
966
|
+
editor.commands.setContent(
|
|
967
|
+
tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
el.promoteLeadingH1Title({ force: true });
|
|
971
|
+
await el.updateComplete;
|
|
972
|
+
|
|
973
|
+
const data = el.getData();
|
|
974
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
975
|
+
|
|
976
|
+
expect(data.title).toBe("Manual title");
|
|
977
|
+
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Markdown Title"));
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("does not promote a leading H1 for link posts", async () => {
|
|
981
|
+
const el = await createElement("link");
|
|
982
|
+
const editor = requireEditor(el);
|
|
983
|
+
|
|
984
|
+
editor.commands.setContent(
|
|
985
|
+
tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
el.promoteLeadingH1Title({ force: true });
|
|
989
|
+
await el.updateComplete;
|
|
990
|
+
|
|
991
|
+
const data = el.getData();
|
|
992
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
993
|
+
|
|
994
|
+
expect(data.title).toBe("");
|
|
995
|
+
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Markdown Title"));
|
|
996
|
+
});
|
|
997
|
+
|
|
908
998
|
it("omits rating when the rating control is hidden", async () => {
|
|
909
999
|
const el = await createElement("note");
|
|
910
1000
|
el._rating = 3;
|
|
@@ -1016,7 +1106,9 @@ describe("JantComposeEditor", () => {
|
|
|
1016
1106
|
image.dispatchEvent(new Event("error"));
|
|
1017
1107
|
await el.updateComplete;
|
|
1018
1108
|
|
|
1019
|
-
|
|
1109
|
+
const fallback = el.querySelector("[data-preview-failed='image']");
|
|
1110
|
+
expect(fallback).not.toBeNull();
|
|
1111
|
+
expect(fallback?.textContent).toContain("Image unavailable");
|
|
1020
1112
|
expect(el.querySelector(".compose-attachment-remove")).not.toBeNull();
|
|
1021
1113
|
expect(el.querySelector(".compose-attachment-img")).toBeNull();
|
|
1022
1114
|
});
|
|
@@ -104,6 +104,46 @@ const suggestedLinks: NavManagerSuggestedLink[] = [
|
|
|
104
104
|
},
|
|
105
105
|
];
|
|
106
106
|
|
|
107
|
+
function renderHeaderFragment(label: string): string {
|
|
108
|
+
return `
|
|
109
|
+
<header class="site-header" data-site-header-fragment="header">
|
|
110
|
+
<div class="site-header-inner">
|
|
111
|
+
<div class="site-header-top">
|
|
112
|
+
<a href="/" class="site-logo">Test Site</a>
|
|
113
|
+
<nav class="site-header-nav" aria-label="Primary">
|
|
114
|
+
<a href="/now" class="site-header-link">${label}</a>
|
|
115
|
+
</nav>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
class="site-header-hamburger"
|
|
119
|
+
aria-controls="site-nav-drawer"
|
|
120
|
+
aria-expanded="false"
|
|
121
|
+
></button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</header>
|
|
125
|
+
<div
|
|
126
|
+
class="site-nav-drawer-backdrop"
|
|
127
|
+
data-site-header-fragment="drawer-backdrop"
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
></div>
|
|
130
|
+
<div
|
|
131
|
+
id="site-nav-drawer"
|
|
132
|
+
class="site-nav-drawer"
|
|
133
|
+
data-site-header-fragment="drawer"
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
inert
|
|
136
|
+
>
|
|
137
|
+
<button class="site-nav-drawer-close" type="button"></button>
|
|
138
|
+
<a href="/now" class="site-nav-drawer-link">${label}</a>
|
|
139
|
+
</div>
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function installCurrentHeaderFragment(label = "Old"): void {
|
|
144
|
+
document.body.insertAdjacentHTML("afterbegin", renderHeaderFragment(label));
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
function requireElement<T>(value: T | null, message: string): T {
|
|
108
148
|
if (!value) {
|
|
109
149
|
throw new Error(message);
|
|
@@ -148,6 +188,12 @@ function getSortableOptions(
|
|
|
148
188
|
return options as Record<string, ((event: unknown) => void) | undefined>;
|
|
149
189
|
}
|
|
150
190
|
|
|
191
|
+
async function flushAsyncWork(): Promise<void> {
|
|
192
|
+
await Promise.resolve();
|
|
193
|
+
await Promise.resolve();
|
|
194
|
+
await Promise.resolve();
|
|
195
|
+
}
|
|
196
|
+
|
|
151
197
|
async function createElement(): Promise<JantNavManager> {
|
|
152
198
|
const el = document.createElement("jant-nav-manager") as JantNavManager;
|
|
153
199
|
el.labels = labels;
|
|
@@ -170,6 +216,7 @@ describe("JantNavManager", () => {
|
|
|
170
216
|
vi.fn().mockResolvedValue({
|
|
171
217
|
ok: true,
|
|
172
218
|
status: 200,
|
|
219
|
+
json: async () => ({ ...items[1] }),
|
|
173
220
|
}),
|
|
174
221
|
);
|
|
175
222
|
});
|
|
@@ -209,8 +256,7 @@ describe("JantNavManager", () => {
|
|
|
209
256
|
});
|
|
210
257
|
|
|
211
258
|
await el.updateComplete;
|
|
212
|
-
await
|
|
213
|
-
await Promise.resolve();
|
|
259
|
+
await flushAsyncWork();
|
|
214
260
|
|
|
215
261
|
expect(getListIds(headerList)).toEqual(["nav-1"]);
|
|
216
262
|
expect(getListIds(moreList)).toEqual(["nav-2", "nav-3"]);
|
|
@@ -234,6 +280,9 @@ describe("JantNavManager", () => {
|
|
|
234
280
|
"/api/nav-items/nav-2/move",
|
|
235
281
|
expect.objectContaining({
|
|
236
282
|
method: "PUT",
|
|
283
|
+
headers: expect.objectContaining({
|
|
284
|
+
"X-Jant-Site-Header": "include",
|
|
285
|
+
}),
|
|
237
286
|
body: JSON.stringify({
|
|
238
287
|
after: null,
|
|
239
288
|
before: "nav-3",
|
|
@@ -293,22 +342,28 @@ describe("JantNavManager", () => {
|
|
|
293
342
|
url: "/now",
|
|
294
343
|
placement: "header",
|
|
295
344
|
};
|
|
345
|
+
installCurrentHeaderFragment("Old");
|
|
296
346
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
297
347
|
ok: true,
|
|
298
348
|
status: 201,
|
|
299
|
-
json: async () =>
|
|
349
|
+
json: async () => ({
|
|
350
|
+
...created,
|
|
351
|
+
headerHtml: renderHeaderFragment("Now"),
|
|
352
|
+
}),
|
|
300
353
|
});
|
|
301
354
|
vi.stubGlobal("fetch", fetchMock);
|
|
302
355
|
|
|
303
356
|
addButton.click();
|
|
304
|
-
await
|
|
305
|
-
await Promise.resolve();
|
|
357
|
+
await flushAsyncWork();
|
|
306
358
|
await el.updateComplete;
|
|
307
359
|
|
|
308
360
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
309
361
|
"/api/nav-items",
|
|
310
362
|
expect.objectContaining({
|
|
311
363
|
method: "POST",
|
|
364
|
+
headers: expect.objectContaining({
|
|
365
|
+
"X-Jant-Site-Header": "include",
|
|
366
|
+
}),
|
|
312
367
|
body: JSON.stringify({
|
|
313
368
|
type: "collection",
|
|
314
369
|
collectionId: "col_now",
|
|
@@ -325,12 +380,25 @@ describe("JantNavManager", () => {
|
|
|
325
380
|
),
|
|
326
381
|
).toContain("nav-now");
|
|
327
382
|
expect(el.querySelector(".nav-suggestion-item")).toBeNull();
|
|
383
|
+
expect(
|
|
384
|
+
document
|
|
385
|
+
.querySelector<HTMLElement>('[data-site-header-fragment="header"]')
|
|
386
|
+
?.textContent?.trim(),
|
|
387
|
+
).toContain("Now");
|
|
328
388
|
});
|
|
329
389
|
|
|
330
|
-
it("confirms before
|
|
390
|
+
it("confirms before deleting a navigation item", async () => {
|
|
331
391
|
const el = await createElement();
|
|
332
|
-
const
|
|
333
|
-
|
|
392
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
393
|
+
ok: true,
|
|
394
|
+
status: 200,
|
|
395
|
+
json: async () => ({
|
|
396
|
+
success: true,
|
|
397
|
+
headerHtml: renderHeaderFragment("Links"),
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
401
|
+
installCurrentHeaderFragment("About");
|
|
334
402
|
|
|
335
403
|
(
|
|
336
404
|
el as unknown as { _editingId: string | null; requestUpdate: () => void }
|
|
@@ -343,7 +411,7 @@ describe("JantNavManager", () => {
|
|
|
343
411
|
"expected nav delete button",
|
|
344
412
|
);
|
|
345
413
|
deleteButton.click();
|
|
346
|
-
await
|
|
414
|
+
await flushAsyncWork();
|
|
347
415
|
|
|
348
416
|
const host = requireElement(
|
|
349
417
|
document.querySelector<HTMLElement>("jant-confirm-dialog"),
|
|
@@ -356,19 +424,37 @@ describe("JantNavManager", () => {
|
|
|
356
424
|
"expected confirm button",
|
|
357
425
|
);
|
|
358
426
|
confirmButton.click();
|
|
359
|
-
await
|
|
360
|
-
await
|
|
427
|
+
await flushAsyncWork();
|
|
428
|
+
await el.updateComplete;
|
|
361
429
|
|
|
362
|
-
expect(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
430
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
431
|
+
"/api/nav-items/nav-1",
|
|
432
|
+
expect.objectContaining({
|
|
433
|
+
method: "DELETE",
|
|
434
|
+
headers: expect.objectContaining({
|
|
435
|
+
"X-Jant-Site-Header": "include",
|
|
436
|
+
}),
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
expect(
|
|
440
|
+
getListIds(
|
|
441
|
+
requireElement(
|
|
442
|
+
el.querySelector<HTMLElement>("#nav-items-header"),
|
|
443
|
+
"expected header nav list",
|
|
444
|
+
),
|
|
445
|
+
),
|
|
446
|
+
).toEqual(["nav-2"]);
|
|
447
|
+
expect(
|
|
448
|
+
document
|
|
449
|
+
.querySelector<HTMLElement>('[data-site-header-fragment="header"]')
|
|
450
|
+
?.textContent?.trim(),
|
|
451
|
+
).toContain("Links");
|
|
366
452
|
});
|
|
367
453
|
|
|
368
|
-
it("does not
|
|
454
|
+
it("does not delete when confirmation is canceled", async () => {
|
|
369
455
|
const el = await createElement();
|
|
370
|
-
const
|
|
371
|
-
|
|
456
|
+
const fetchMock = vi.fn();
|
|
457
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
372
458
|
|
|
373
459
|
(
|
|
374
460
|
el as unknown as { _editingId: string | null; requestUpdate: () => void }
|
|
@@ -381,7 +467,7 @@ describe("JantNavManager", () => {
|
|
|
381
467
|
"expected nav delete button",
|
|
382
468
|
);
|
|
383
469
|
deleteButton.click();
|
|
384
|
-
await
|
|
470
|
+
await flushAsyncWork();
|
|
385
471
|
|
|
386
472
|
const host = requireElement(
|
|
387
473
|
document.querySelector<HTMLElement>("jant-confirm-dialog"),
|
|
@@ -394,9 +480,8 @@ describe("JantNavManager", () => {
|
|
|
394
480
|
"expected cancel button",
|
|
395
481
|
);
|
|
396
482
|
cancelButton.click();
|
|
397
|
-
await
|
|
398
|
-
await Promise.resolve();
|
|
483
|
+
await flushAsyncWork();
|
|
399
484
|
|
|
400
|
-
expect(
|
|
485
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
401
486
|
});
|
|
402
487
|
});
|
|
@@ -657,6 +657,7 @@ describe("JantSettingsGeneral", () => {
|
|
|
657
657
|
const d = detail as unknown as SettingsSaveDetail;
|
|
658
658
|
expect(d.endpoint).toBe("/settings/general/home");
|
|
659
659
|
expect(d.section).toBe("home");
|
|
660
|
+
expect(d.data).not.toHaveProperty("homeDefaultView");
|
|
660
661
|
expect(d.data.showJantBrandingOnHome).toBe(true);
|
|
661
662
|
expect(siteSaveBtn?.disabled).toBe(true);
|
|
662
663
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { JSONContent } from "@tiptap/core";
|
|
2
|
+
|
|
3
|
+
export interface LeadingH1TitlePromotion {
|
|
4
|
+
title: string;
|
|
5
|
+
bodyJson: JSONContent | null;
|
|
6
|
+
headingIndex: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function textFromNode(node: JSONContent): string {
|
|
10
|
+
if (typeof node.text === "string") return node.text;
|
|
11
|
+
if (!node.content) return node.type === "hardBreak" ? " " : "";
|
|
12
|
+
return node.content.map(textFromNode).join("");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasOnlyEmptyInlineContent(node: JSONContent): boolean {
|
|
16
|
+
if (!node.content || node.content.length === 0) return true;
|
|
17
|
+
return node.content.every((child) => {
|
|
18
|
+
if (child.type === "text") return !child.text?.trim();
|
|
19
|
+
if (child.type === "hardBreak") return true;
|
|
20
|
+
return false;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isIgnorableLeadingBlock(node: JSONContent): boolean {
|
|
25
|
+
return (
|
|
26
|
+
(node.type === "paragraph" || node.type === "heading") &&
|
|
27
|
+
hasOnlyEmptyInlineContent(node)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeTitleText(text: string): string {
|
|
32
|
+
return text.replace(/\s+/g, " ").trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function promoteLeadingH1Title(
|
|
36
|
+
bodyJson: JSONContent | null,
|
|
37
|
+
): LeadingH1TitlePromotion | null {
|
|
38
|
+
const content = bodyJson?.content;
|
|
39
|
+
if (!bodyJson || !content?.length) return null;
|
|
40
|
+
|
|
41
|
+
const headingIndex = content.findIndex((node) => {
|
|
42
|
+
return !isIgnorableLeadingBlock(node);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (headingIndex < 0) return null;
|
|
46
|
+
|
|
47
|
+
const heading = content[headingIndex];
|
|
48
|
+
if (heading?.type !== "heading" || heading.attrs?.level !== 1) return null;
|
|
49
|
+
|
|
50
|
+
const title = normalizeTitleText(textFromNode(heading));
|
|
51
|
+
if (!title) return null;
|
|
52
|
+
|
|
53
|
+
const nextContent = content.slice(headingIndex + 1);
|
|
54
|
+
if (nextContent.length === 0) {
|
|
55
|
+
return { title, bodyJson: null, headingIndex };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
title,
|
|
60
|
+
bodyJson: {
|
|
61
|
+
...bodyJson,
|
|
62
|
+
content: nextContent,
|
|
63
|
+
},
|
|
64
|
+
headingIndex,
|
|
65
|
+
};
|
|
66
|
+
}
|