fix: consistent handling of markdown between post preview and preview server

This commit is contained in:
2026-02-16 22:04:55 +01:00
parent e98379fe95
commit 201a74f447
2 changed files with 29 additions and 28 deletions

View File

@@ -19,6 +19,7 @@ import { AutoSaveManager, getContrastColor } from '../../utils';
import { parseMacros, getMacro } from '../../macros/registry'; import { parseMacros, getMacro } from '../../macros/registry';
import { InsertModal } from '../InsertModal'; import { InsertModal } from '../InsertModal';
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal'; import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
import { marked } from 'marked';
import './Editor.css'; import './Editor.css';
/** Get display name for media: prefer title over originalName */ /** Get display name for media: prefer title over originalName */
@@ -134,7 +135,7 @@ const renderMacroSync = (name: string, params: Record<string, string>, postId?:
}; };
// Simple markdown to HTML converter for preview // Simple markdown to HTML converter for preview
const markdownToHtml = (markdown: string, postId?: string): string => { export const markdownToHtml = (markdown: string, postId?: string): string => {
// First, render macros // First, render macros
const macros = parseMacros(markdown); const macros = parseMacros(markdown);
let result = markdown; let result = markdown;
@@ -145,33 +146,12 @@ const markdownToHtml = (markdown: string, postId?: string): string => {
const rendered = renderMacroSync(macro.name, macro.params, postId); const rendered = renderMacroSync(macro.name, macro.params, postId);
result = result.slice(0, macro.start) + rendered + result.slice(macro.end); result = result.slice(0, macro.start) + rendered + result.slice(macro.end);
} }
return result return marked.parse(result, {
// Escape HTML (but not our rendered macros - they're already safe) gfm: true,
// We need to be careful here - macro output contains HTML breaks: false,
// For safety, we skip escaping since we control the macro output async: false,
// Headers }) as string;
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
// Bold
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
// Italic
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
// Images
.replace(/!\[(.*?)\]\((.*?)\)/gim, '<img alt="$1" src="$2" style="max-width: 100%;" />')
// Links
.replace(/\[(.*?)\]\((.*?)\)/gim, '<a href="$2" target="_blank">$1</a>')
// Code blocks
.replace(/```([\s\S]*?)```/gim, '<pre><code>$1</code></pre>')
// Inline code
.replace(/`(.*?)`/gim, '<code>$1</code>')
// Blockquotes
.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>')
// Horizontal rules
.replace(/^---$/gim, '<hr />')
// Line breaks
.replace(/\n/g, '<br />');
}; };
/** /**

View File

@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest';
import { markdownToHtml } from '../../../src/renderer/components/Editor/Editor';
describe('Editor markdown preview rendering', () => {
it('renders continuous blockquote lines as a single blockquote paragraph (CommonMark softbreak behavior)', () => {
const markdown = [
'> Georg Bauer',
'> Am Krug 40',
'> 48151 Münster',
'> eMail: gb at rfc1437.de',
].join('\n');
const html = markdownToHtml(markdown, 'post-1');
expect((html.match(/<blockquote>/g) ?? []).length).toBe(1);
expect((html.match(/<p>/g) ?? []).length).toBe(1);
expect(html).not.toContain('<br');
expect(html).toContain('Georg Bauer');
expect(html).toContain('eMail: gb at rfc1437.de');
});
});