diff --git a/src/main/engine/BlogGenerationEngine.ts b/src/main/engine/BlogGenerationEngine.ts index f2e62bf..3209a16 100644 --- a/src/main/engine/BlogGenerationEngine.ts +++ b/src/main/engine/BlogGenerationEngine.ts @@ -106,6 +106,11 @@ export interface SiteValidationApplyResult { removedEmptyDirCount: number; } +export interface CalendarRegenerationResult { + calendarPath: string; + changed: boolean; +} + export function resolvePublicBaseUrl(publicUrl?: string): string | null { const trimmed = (publicUrl || '').trim(); if (!trimmed) { @@ -473,6 +478,41 @@ export class BlogGenerationEngine { }; } + async regenerateCalendar( + options: BlogGenerationOptions, + onProgress: (progress: number, message?: string) => void, + ): Promise { + onProgress(0, 'Loading posts...'); + + const categorySettings = resolveCategorySettings(options.categoryMetadata, options.categorySettings); + const listExcludedCategories = Object.entries(categorySettings) + .filter(([, settings]) => settings.renderInLists === false) + .map(([category]) => category); + + const { publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories); + + onProgress(50, 'Building calendar data...'); + + const calendarJson = `${JSON.stringify(buildCalendarArchiveData(publishedListPosts), null, 2)}\n`; + const htmlDir = path.join(options.dataDir, 'html'); + await fs.mkdir(htmlDir, { recursive: true }); + const calendarPath = path.join(htmlDir, 'calendar.json'); + + const changed = await writeFileIfHashChanged({ + projectId: options.projectId, + filePath: calendarPath, + relativePath: 'calendar.json', + content: calendarJson, + }); + + onProgress(100, 'Calendar data regenerated'); + + return { + calendarPath, + changed, + }; + } + async validateSite( options: BlogGenerationOptions, onProgress: (progress: number, message?: string) => void, @@ -748,6 +788,14 @@ export class BlogGenerationEngine { } } + if (renderedUrlCount > 0 || deletedUrlCount > 0) { + onProgress(90, 'Regenerating calendar data...'); + await this.regenerateCalendar(options, (progress, message) => { + const mappedProgress = 90 + Math.floor((progress / 100) * 9); + onProgress(Math.min(99, mappedProgress), message || 'Regenerating calendar data...'); + }); + } + onProgress(100, `Apply complete (${deletedUrlCount} deleted, ${renderedUrlCount} rendered)`); return { diff --git a/src/main/engine/PreviewServer.ts b/src/main/engine/PreviewServer.ts index 84f222a..e037d23 100644 --- a/src/main/engine/PreviewServer.ts +++ b/src/main/engine/PreviewServer.ts @@ -28,6 +28,8 @@ import { loadPublishedSnapshots, loadPublishedSnapshotsPage, } from './SharedSnapshotService'; +import { buildCalendarArchiveData } from './GenerationSitemapFeedService'; +import { loadPublishedGenerationSets } from './GenerationPostSnapshotService'; interface ActiveProjectContext { projectId: string; @@ -254,6 +256,12 @@ export class PreviewServer { const picoStylesheetHref = getPicoStylesheetHref(appliedTheme); const htmlRewriteContext = await this.buildHtmlRewriteContext(); + if (pathname === '/calendar.json') { + const calendarJson = await this.resolveCalendarJson(context.dataDir, listExcludedCategories); + this.respondAsset(res, 'application/json; charset=utf-8', calendarJson); + return; + } + if (pathname === '/__style-preview') { const stylePreviewHtml = await this.renderStylePreview(htmlRewriteContext, { pageTitle, @@ -491,6 +499,21 @@ export class PreviewServer { } } + private async resolveCalendarJson(dataDir: string | undefined, listExcludedCategories: string[]): Promise { + if (dataDir) { + const calendarPath = path.join(dataDir, 'html', 'calendar.json'); + try { + return await readFile(calendarPath); + } catch { + // fall through to dynamic generation for preview runtime + } + } + + const { publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories); + const calendarJson = `${JSON.stringify(buildCalendarArchiveData(publishedListPosts), null, 2)}\n`; + return Buffer.from(calendarJson, 'utf-8'); + } + private getMediaContentType(filePath: string): string { const extension = path.extname(filePath).toLowerCase(); switch (extension) { diff --git a/src/main/engine/templates/partials/styles.liquid b/src/main/engine/templates/partials/styles.liquid index 2b39043..d742268 100644 --- a/src/main/engine/templates/partials/styles.liquid +++ b/src/main/engine/templates/partials/styles.liquid @@ -17,19 +17,30 @@ .blog-menu-submenu .blog-menu-list { flex-direction: column; flex-wrap: nowrap; gap: .2rem; } .blog-menu-item-with-children:hover > .blog-menu-submenu, .blog-menu-item-with-children:focus-within > .blog-menu-submenu { display: block; } - .blog-menu-calendar { position: relative; display: inline-flex; align-items: center; justify-content: center; margin-left: auto; } - .blog-menu-calendar-button { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; padding: 0; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: var(--pico-card-background-color, var(--card-background-color)); color: var(--pico-muted-color, var(--muted-color)); border-radius: .25rem; cursor: pointer; } - .blog-menu-calendar-button svg { display: block; width: 1rem; height: 1rem; fill: none; stroke: currentColor; } + .blog-menu-calendar { position: relative; display: inline-flex; align-items: center; justify-content: center; margin-left: auto; align-self: center; } + .blog-menu-calendar-button { display: inline-flex; align-items: center; justify-content: center; width: auto; height: auto; padding: .2rem .1rem; border: 0; background: transparent; color: var(--pico-muted-color, var(--muted-color)); border-radius: 0; cursor: pointer; font-size: .94rem; line-height: 1.4; } + .blog-menu-calendar-button svg { display: block; width: .94rem; height: .94rem; fill: none; stroke: currentColor; transform: translateY(1px); } .blog-menu-calendar-button:hover, - .blog-menu-calendar-button:focus-visible { color: var(--pico-color, var(--color)); border-color: var(--pico-color, var(--color)); } - .blog-calendar-panel { position: absolute; top: calc(100% + .3rem); right: 0; width: min(21.5rem, 92vw); border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: var(--pico-card-background-color, var(--card-background-color)); padding: .55rem; z-index: 30; } - .blog-calendar-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: .35rem; } - .blog-calendar-close { border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: transparent; color: var(--pico-muted-color, var(--muted-color)); width: 1.8rem; height: 1.8rem; border-radius: .2rem; padding: 0; cursor: pointer; } + .blog-menu-calendar-button:focus-visible { color: var(--pico-color, var(--color)); } + .blog-calendar-panel { position: absolute; top: calc(100% + .2rem); right: 0; width: min(19rem, 92vw); border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: var(--pico-card-background-color, var(--card-background-color)); padding: .4rem; z-index: 30; } + .blog-calendar-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: .2rem; } + .blog-calendar-close { border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: transparent; color: var(--pico-muted-color, var(--muted-color)); width: 1.55rem; height: 1.55rem; border-radius: .2rem; padding: 0; cursor: pointer; } .blog-calendar-close:hover, .blog-calendar-close:focus-visible { color: var(--pico-color, var(--color)); border-color: var(--pico-color, var(--color)); } - .blog-calendar-status { margin: .45rem 0 0; color: var(--pico-muted-color, var(--muted-color)); font-size: .85rem; } + .blog-calendar-content { display: grid; gap: .18rem; } + .blog-calendar-status { margin: .2rem 0 0; color: var(--pico-muted-color, var(--muted-color)); font-size: .78rem; } + [data-blog-calendar-root] [data-vc=header] { margin-bottom: .2rem; } + [data-blog-calendar-root] [data-vc=month], + [data-blog-calendar-root] [data-vc=year] { padding: .15rem .25rem; } + [data-blog-calendar-root] [data-vc=months], + [data-blog-calendar-root] [data-vc=years] { row-gap: .5rem; } + [data-blog-calendar-root] [data-vc-months-month], + [data-blog-calendar-root] [data-vc-years-year] { height: 2rem; } + [data-blog-calendar-root] [data-vc-week=days] { margin-bottom: .25rem; } + [data-blog-calendar-root] [data-vc-date] { padding-top: 0; padding-bottom: 0; } + [data-blog-calendar-root] [data-vc-date-btn] { min-height: 1.65rem; min-width: 1.65rem; } [data-blog-calendar-has-posts='true'] [data-vc-date-btn] { border-color: var(--pico-primary, var(--pico-color, var(--color))); } - .blog-calendar-post-count { display: inline-flex; align-items: center; justify-content: center; min-width: 1.15rem; height: 1.15rem; margin-left: .22rem; border-radius: 999px; font-size: .62rem; line-height: 1; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); color: var(--pico-color, var(--color)); } + .blog-calendar-post-count { display: inline-flex; align-items: center; justify-content: center; min-width: .95rem; height: .95rem; margin-left: .16rem; border-radius: 999px; font-size: .56rem; line-height: 1; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); color: var(--pico-color, var(--color)); } .post { border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); padding: 1rem; background: var(--pico-card-background-color, var(--card-background-color)); min-width: 0; } .post pre { position: relative; overflow-x: auto; max-width: 100%; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); border-radius: .3rem; margin: .9rem 0; padding: .85rem .9rem; background: var(--pico-code-background-color, rgba(33, 38, 45, .82)); box-sizing: border-box; } .post pre code { display: block; font-size: .88rem; line-height: 1.5; white-space: pre; } diff --git a/src/main/ipc/blogHandlers.ts b/src/main/ipc/blogHandlers.ts index 19e4ecd..713e2e2 100644 --- a/src/main/ipc/blogHandlers.ts +++ b/src/main/ipc/blogHandlers.ts @@ -152,6 +152,22 @@ export function registerBlogHandlers(safeHandle: SafeHandle): void { }); }); + safeHandle('blog:regenerateCalendar', async () => { + const blogGenerationEngine = getBlogGenerationEngine(); + const baseOptions = await resolveBlogGenerationBaseOptions(); + + const taskTimestamp = Date.now(); + return taskManager.runTask({ + id: `site-calendar-regenerate-${taskTimestamp}`, + name: 'Regenerate Calendar', + execute: async (onProgress) => { + return blogGenerationEngine.regenerateCalendar(baseOptions, (progress, message) => { + onProgress(progress, message || 'Regenerating calendar...'); + }); + }, + }); + }); + safeHandle('blog:applyValidation', async (_event, report: SiteValidationReport) => { const blogGenerationEngine = getBlogGenerationEngine(); const baseOptions = await resolveBlogGenerationBaseOptions(); diff --git a/src/main/preload.ts b/src/main/preload.ts index 4945a2b..1a4307f 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -256,6 +256,7 @@ export const electronAPI: ElectronAPI = { generateSitemap: () => ipcRenderer.invoke('blog:generateSitemap'), validateSite: () => ipcRenderer.invoke('blog:validateSite'), applyValidation: (report: SiteValidationReport) => ipcRenderer.invoke('blog:applyValidation', report), + regenerateCalendar: () => ipcRenderer.invoke('blog:regenerateCalendar'), }, menu: { diff --git a/src/main/shared/electronApi.ts b/src/main/shared/electronApi.ts index 12007da..dfea63f 100644 --- a/src/main/shared/electronApi.ts +++ b/src/main/shared/electronApi.ts @@ -427,6 +427,11 @@ export interface SiteValidationApplyResult { removedEmptyDirCount: number; } +export interface CalendarRegenerationResult { + calendarPath: string; + changed: boolean; +} + export type MenuItemKind = 'page' | 'submenu' | 'category-archive' | 'home'; export interface MenuItemData { @@ -649,6 +654,7 @@ export interface ElectronAPI { }>; validateSite: () => Promise; applyValidation: (report: SiteValidationReport) => Promise; + regenerateCalendar: () => Promise; }; menu: { get: () => Promise; diff --git a/src/main/shared/i18n/locales/de.json b/src/main/shared/i18n/locales/de.json index 50bf564..5f3172b 100644 --- a/src/main/shared/i18n/locales/de.json +++ b/src/main/shared/i18n/locales/de.json @@ -38,6 +38,7 @@ "menu.item.metadataDiff": "Metadaten-Diff-Werkzeug", "menu.item.editMenu": "Blog-Menü bearbeiten", "menu.item.generateSitemap": "Site rendern", + "menu.item.regenerateCalendar": "Kalender neu erzeugen", "menu.item.validateSite": "Website validieren", "menu.item.about": "Über Blogging Desktop Server", "menu.item.openDocumentation": "Dokumentation öffnen", diff --git a/src/main/shared/i18n/locales/en.json b/src/main/shared/i18n/locales/en.json index 366a04c..a99974d 100644 --- a/src/main/shared/i18n/locales/en.json +++ b/src/main/shared/i18n/locales/en.json @@ -38,6 +38,7 @@ "menu.item.metadataDiff": "Metadata Diff Tool", "menu.item.editMenu": "Edit Blog Menu", "menu.item.generateSitemap": "Render Site", + "menu.item.regenerateCalendar": "Regenerate Calendar", "menu.item.validateSite": "Validate Site", "menu.item.about": "About Blogging Desktop Server", "menu.item.openDocumentation": "Open Documentation", diff --git a/src/main/shared/i18n/locales/es.json b/src/main/shared/i18n/locales/es.json index 700bb74..d77db16 100644 --- a/src/main/shared/i18n/locales/es.json +++ b/src/main/shared/i18n/locales/es.json @@ -38,6 +38,7 @@ "menu.item.metadataDiff": "Herramienta diff de metadatos", "menu.item.editMenu": "Editar menú del blog", "menu.item.generateSitemap": "Renderizar sitio", + "menu.item.regenerateCalendar": "Regenerar calendario", "menu.item.validateSite": "Validar sitio", "menu.item.about": "Acerca de Blogging Desktop Server", "menu.item.openDocumentation": "Abrir documentación", diff --git a/src/main/shared/i18n/locales/fr.json b/src/main/shared/i18n/locales/fr.json index a8a0b53..ee4a901 100644 --- a/src/main/shared/i18n/locales/fr.json +++ b/src/main/shared/i18n/locales/fr.json @@ -38,6 +38,7 @@ "menu.item.metadataDiff": "Outil de diff des métadonnées", "menu.item.editMenu": "Modifier le menu du blog", "menu.item.generateSitemap": "Rendre le site", + "menu.item.regenerateCalendar": "Régénérer le calendrier", "menu.item.validateSite": "Valider le site", "menu.item.about": "À propos de Blogging Desktop Server", "menu.item.openDocumentation": "Ouvrir la documentation", diff --git a/src/main/shared/i18n/locales/it.json b/src/main/shared/i18n/locales/it.json index 04452ff..70a8d30 100644 --- a/src/main/shared/i18n/locales/it.json +++ b/src/main/shared/i18n/locales/it.json @@ -38,6 +38,7 @@ "menu.item.metadataDiff": "Strumento diff metadati", "menu.item.editMenu": "Modifica menu blog", "menu.item.generateSitemap": "Renderizza sito", + "menu.item.regenerateCalendar": "Rigenera calendario", "menu.item.validateSite": "Valida sito", "menu.item.about": "Informazioni su Blogging Desktop Server", "menu.item.openDocumentation": "Apri documentazione", diff --git a/src/main/shared/menuCommands.ts b/src/main/shared/menuCommands.ts index e623d52..2f01d1d 100644 --- a/src/main/shared/menuCommands.ts +++ b/src/main/shared/menuCommands.ts @@ -33,6 +33,7 @@ export type AppMenuAction = | 'metadataDiff' | 'editMenu' | 'generateSitemap' + | 'regenerateCalendar' | 'validateSite' | 'openDocumentation' | 'about' @@ -126,6 +127,7 @@ export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [ { label: 'menu.item.metadataDiff', action: 'metadataDiff' }, { label: 'menu.item.editMenu', action: 'editMenu' }, { label: 'menu.item.generateSitemap', action: 'generateSitemap', accelerator: 'CmdOrCtrl+R' }, + { label: 'menu.item.regenerateCalendar', action: 'regenerateCalendar' }, { label: 'menu.item.validateSite', action: 'validateSite', accelerator: 'CmdOrCtrl+Shift+L' }, ], }, @@ -160,6 +162,7 @@ export const APP_MENU_ACTION_EVENT_MAP: Partial> = metadataDiff: 'menu:metadataDiff', editMenu: 'menu:editMenu', generateSitemap: 'menu:generateSitemap', + regenerateCalendar: 'menu:regenerateCalendar', validateSite: 'menu:validateSite', openDocumentation: 'menu:openDocumentation', about: 'menu:about', diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 890a7d8..beb411a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -361,6 +361,17 @@ const App: React.FC = () => { }) || (() => {}) ); + unsubscribers.push( + window.electronAPI?.on('menu:regenerateCalendar', async () => { + try { + await window.electronAPI?.blog.regenerateCalendar(); + } catch (error) { + console.error('Calendar regeneration failed:', error); + showToast.error(tr('app.calendarRegenerationFailed')); + } + }) || (() => {}) + ); + unsubscribers.push( window.electronAPI?.on('menu:validateSite', () => { const validateAndOpen = async () => { diff --git a/src/renderer/i18n/locales/de.json b/src/renderer/i18n/locales/de.json index 45bbf15..c441ce4 100644 --- a/src/renderer/i18n/locales/de.json +++ b/src/renderer/i18n/locales/de.json @@ -25,6 +25,7 @@ "app.databaseRebuildFailed": "Datenbank-Neuaufbau fehlgeschlagen", "app.textReindexFailed": "Text-Neuindizierung fehlgeschlagen", "app.sitemapGenerationFailed": "Sitemap-Erstellung fehlgeschlagen", + "app.calendarRegenerationFailed": "Kalender-Neuerstellung fehlgeschlagen", "app.previewOpenFailed": "Ausgewählte Beitragsvorschau konnte nicht geöffnet werden", "app.metadataDiff": "Metadaten-Diff", "app.importComplete": "Import abgeschlossen: {posts} Beiträge, {media} Mediendateien", diff --git a/src/renderer/i18n/locales/en.json b/src/renderer/i18n/locales/en.json index 3062650..9c93d31 100644 --- a/src/renderer/i18n/locales/en.json +++ b/src/renderer/i18n/locales/en.json @@ -25,6 +25,7 @@ "app.databaseRebuildFailed": "Database rebuild failed", "app.textReindexFailed": "Text reindex failed", "app.sitemapGenerationFailed": "Sitemap generation failed", + "app.calendarRegenerationFailed": "Calendar regeneration failed", "app.previewOpenFailed": "Failed to open selected post preview", "app.metadataDiff": "Metadata Diff", "app.importComplete": "Import complete: {posts} posts, {media} media files", diff --git a/src/renderer/i18n/locales/es.json b/src/renderer/i18n/locales/es.json index 138afd8..bc2ebdc 100644 --- a/src/renderer/i18n/locales/es.json +++ b/src/renderer/i18n/locales/es.json @@ -25,6 +25,7 @@ "app.databaseRebuildFailed": "La reconstrucción de la base de datos falló", "app.textReindexFailed": "La reindexación de texto falló", "app.sitemapGenerationFailed": "La generación del sitemap falló", + "app.calendarRegenerationFailed": "La regeneración del calendario falló", "app.previewOpenFailed": "No se pudo abrir la vista previa de la entrada seleccionada", "app.metadataDiff": "Diferencia de Metadatos", "app.importComplete": "Importación completada: {posts} entradas, {media} archivos multimedia", diff --git a/src/renderer/i18n/locales/fr.json b/src/renderer/i18n/locales/fr.json index 786d5f5..f8fab79 100644 --- a/src/renderer/i18n/locales/fr.json +++ b/src/renderer/i18n/locales/fr.json @@ -25,6 +25,7 @@ "app.databaseRebuildFailed": "Échec de la reconstruction de la base de données", "app.textReindexFailed": "Échec de la réindexation du texte", "app.sitemapGenerationFailed": "Échec de la génération du sitemap", + "app.calendarRegenerationFailed": "Échec de la régénération du calendrier", "app.previewOpenFailed": "Impossible d’ouvrir l’aperçu de l’article sélectionné", "app.metadataDiff": "Diff Métadonnées", "app.importComplete": "Import terminé : {posts} articles, {media} fichiers média", diff --git a/src/renderer/i18n/locales/it.json b/src/renderer/i18n/locales/it.json index d5156a3..2485e50 100644 --- a/src/renderer/i18n/locales/it.json +++ b/src/renderer/i18n/locales/it.json @@ -25,6 +25,7 @@ "app.databaseRebuildFailed": "Ricostruzione database non riuscita", "app.textReindexFailed": "Reindicizzazione testo non riuscita", "app.sitemapGenerationFailed": "Generazione sitemap non riuscita", + "app.calendarRegenerationFailed": "Rigenerazione del calendario non riuscita", "app.previewOpenFailed": "Impossibile aprire l’anteprima del post selezionato", "app.metadataDiff": "Diff Metadati", "app.importComplete": "Import completato: {posts} post, {media} file multimediali", diff --git a/tests/engine/BlogGenerationEngine.test.ts b/tests/engine/BlogGenerationEngine.test.ts index bf5ead6..92d7b5d 100644 --- a/tests/engine/BlogGenerationEngine.test.ts +++ b/tests/engine/BlogGenerationEngine.test.ts @@ -994,6 +994,16 @@ describe('BlogGenerationEngine', () => { expect(await fileExists(path.join(tempDir, 'html', 'obsolete'))).toBe(false); expect(await fileExists(path.join(tempDir, 'html'))).toBe(true); expect(await fileExists(path.join(tempDir, 'html', '2025', '01', '15', 'apply-post', 'index.html'))).toBe(true); + + const calendarJsonRaw = await readFile(path.join(tempDir, 'html', 'calendar.json'), 'utf-8'); + const calendarJson = JSON.parse(calendarJsonRaw) as { + years: Record; + months: Record; + days: Record; + }; + expect(calendarJson.years['2025']).toBe(1); + expect(calendarJson.months['2025-01']).toBe(1); + expect(calendarJson.days['2025-01-15']).toBe(1); }); it('does not report valid pagination routes as extra html content', async () => { diff --git a/tests/engine/PreviewServer.test.ts b/tests/engine/PreviewServer.test.ts index 7119567..b926cc4 100644 --- a/tests/engine/PreviewServer.test.ts +++ b/tests/engine/PreviewServer.test.ts @@ -357,6 +357,38 @@ describe('PreviewServer', () => { expect(lightboxLoadingImageResponse.headers.get('content-type')).toContain('image/gif'); }); + it('serves calendar.json for preview calendar runtime', async () => { + tempDir = await mkdtemp(path.join(tmpdir(), 'bds-preview-calendar-')); + await mkdir(path.join(tempDir, 'html'), { recursive: true }); + await writeFile(path.join(tempDir, 'html', 'calendar.json'), JSON.stringify({ + years: { '2025': 2 }, + months: { '2025-01': 2 }, + days: { '2025-01-02': 1, '2025-01-03': 1 }, + }), 'utf-8'); + + server = new PreviewServer({ + postEngine: makeEngine([makePost()]), + settingsEngine: makeSettings(50), + getActiveProjectContext: async () => ({ projectId: 'default', dataDir: tempDir ?? undefined }), + }); + + await server.start(0); + + const response = await fetch(`${server.getBaseUrl()}/calendar.json`); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toContain('application/json'); + + const payload = await response.json() as { + years: Record; + months: Record; + days: Record; + }; + + expect(payload.years['2025']).toBe(2); + expect(payload.months['2025-01']).toBe(2); + expect(payload.days['2025-01-03']).toBe(1); + }); + it('keeps markdown code block html minimal and includes code language metadata', async () => { const postWithCode = makePost({ content: '```python\nprint("hello")\n```', diff --git a/tests/renderer/menuCommands.test.ts b/tests/renderer/menuCommands.test.ts index 141a5c8..a081a44 100644 --- a/tests/renderer/menuCommands.test.ts +++ b/tests/renderer/menuCommands.test.ts @@ -52,6 +52,17 @@ describe('Help menu documentation entry', () => { expect(APP_MENU_ACTION_EVENT_MAP.validateSite).toBe('menu:validateSite'); }); + it('includes Regenerate Calendar action in Blog menu', () => { + const blogGroup = APP_MENU_GROUPS.find((group) => group.label === 'Blog'); + + expect(blogGroup).toBeDefined(); + expect(blogGroup?.items.some((item) => item.action === 'regenerateCalendar')).toBe(true); + }); + + it('maps Regenerate Calendar to a renderer menu event', () => { + expect(APP_MENU_ACTION_EVENT_MAP.regenerateCalendar).toBe('menu:regenerateCalendar'); + }); + it('includes Edit Preferences action in Edit menu with comma shortcut', () => { const editGroup = APP_MENU_GROUPS.find((group) => group.label === 'Edit'); const preferencesItem = editGroup?.items.find((item) => item.action === 'editPreferences');