fix: consistent handling of markdown between post preview and preview server
This commit is contained in:
@@ -19,6 +19,7 @@ import { AutoSaveManager, getContrastColor } from '../../utils';
|
||||
import { parseMacros, getMacro } from '../../macros/registry';
|
||||
import { InsertModal } from '../InsertModal';
|
||||
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
|
||||
import { marked } from 'marked';
|
||||
import './Editor.css';
|
||||
|
||||
/** 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
|
||||
const markdownToHtml = (markdown: string, postId?: string): string => {
|
||||
export const markdownToHtml = (markdown: string, postId?: string): string => {
|
||||
// First, render macros
|
||||
const macros = parseMacros(markdown);
|
||||
let result = markdown;
|
||||
@@ -145,33 +146,12 @@ const markdownToHtml = (markdown: string, postId?: string): string => {
|
||||
const rendered = renderMacroSync(macro.name, macro.params, postId);
|
||||
result = result.slice(0, macro.start) + rendered + result.slice(macro.end);
|
||||
}
|
||||
|
||||
return result
|
||||
// Escape HTML (but not our rendered macros - they're already safe)
|
||||
// We need to be careful here - macro output contains HTML
|
||||
// For safety, we skip escaping since we control the macro output
|
||||
// Headers
|
||||
.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 />');
|
||||
|
||||
return marked.parse(result, {
|
||||
gfm: true,
|
||||
breaks: false,
|
||||
async: false,
|
||||
}) as string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
21
tests/renderer/components/EditorMarkdownPreview.test.ts
Normal file
21
tests/renderer/components/EditorMarkdownPreview.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user