feat: implement minimal server-first markdown processing

Backend implementation:
- Add goldmark dependency for markdown processing
- Create MarkdownProcessor with minimal config (bold, italic, links only)
- Update content injector with HTML injection capabilities
- Add injectHTMLContent() for safe DOM manipulation
- Server now converts **bold**, *italic*, [links](url) to HTML during enhancement

Frontend alignment:
- Restrict marked.js to match server capabilities
- Disable unsupported features (headings, lists, code blocks, tables)
- Update turndown rules to prevent unsupported markdown generation
- Frontend editor preview now matches server output exactly

Server as source of truth:
- Build-time markdown→HTML conversion during enhancement
- Zero runtime overhead for end users
- Consistent formatting between editor preview and final output
- Raw markdown stored in database, HTML served to visitors

Tested features:
- **bold** → <strong>bold</strong> 
- *italic* → <em>italic</em> 
- [text](url) → <a href="url">text</a> 
This commit is contained in:
2025-09-11 16:43:40 +02:00
parent 3db1340cce
commit 350c3f6160
6 changed files with 256 additions and 30 deletions

View File

@@ -14,32 +14,75 @@ export class MarkdownConverter {
}
/**
* Configure marked for HTML output
* Configure marked for HTML output - MINIMAL MODE
* Only supports: **bold**, *italic*, and [links](url)
* Matches server-side goldmark configuration
*/
initializeMarked() {
marked.setOptions({
gfm: true, // GitHub Flavored Markdown
breaks: true, // Convert \n to <br>
gfm: false, // Disable GFM to match server minimal mode
breaks: true, // Convert \n to <br> (matches server)
pedantic: false, // Don't be overly strict
sanitize: false, // Allow HTML (we control the input)
smartLists: true, // Smarter list behavior
smartLists: false, // Disable lists (not supported on server)
smartypants: false // Don't convert quotes/dashes
});
// Override renderers to restrict to minimal feature set
marked.use({
renderer: {
// Disable headings - treat as plain text
heading(text, level) {
return text;
},
// Disable lists - treat as plain text
list(body, ordered, start) {
return body.replace(/<\/?li>/g, '');
},
listitem(text) {
return text + '\n';
},
// Disable code blocks - treat as plain text
code(code, language) {
return code;
},
blockquote(quote) {
return quote; // Disable blockquotes - treat as plain text
},
// Disable horizontal rules
hr() {
return '';
},
// Disable tables
table(header, body) {
return header + body;
},
tablecell(content, flags) {
return content;
},
tablerow(content) {
return content;
}
}
});
}
/**
* Configure turndown for markdown output
* Configure turndown for markdown output - MINIMAL MODE
* Only supports: **bold**, *italic*, and [links](url)
* Matches server-side goldmark configuration
*/
initializeTurndown() {
this.turndown = new TurndownService({
headingStyle: 'atx', // # headers instead of underlines
hr: '---', // horizontal rule style
bulletListMarker: '-', // bullet list marker
codeBlockStyle: 'fenced', // ``` code blocks
fence: '```', // fence marker
emDelimiter: '*', // emphasis delimiter
strongDelimiter: '**', // strong delimiter
linkStyle: 'inlined', // [text](url) instead of reference style
// Minimal configuration - only basic formatting
headingStyle: 'atx', // # headers (but will be disabled)
hr: '---', // horizontal rule (but will be disabled)
bulletListMarker: '-', // bullet list (but will be disabled)
codeBlockStyle: 'fenced', // code blocks (but will be disabled)
fence: '```', // fence marker (but will be disabled)
emDelimiter: '*', // *italic* - matches server
strongDelimiter: '**', // **bold** - matches server
linkStyle: 'inlined', // [text](url) - matches server
linkReferenceStyle: 'full' // full reference links
});
@@ -48,7 +91,9 @@ export class MarkdownConverter {
}
/**
* Add custom turndown rules for better HTML → Markdown conversion
* Add custom turndown rules - MINIMAL MODE
* Only supports: **bold**, *italic*, and [links](url)
* Disables all other formatting to match server
*/
addTurndownRules() {
// Handle paragraph spacing properly - ensure double newlines between paragraphs
@@ -60,7 +105,7 @@ export class MarkdownConverter {
}
});
// Handle bold text in markdown
// Handle bold text in markdown - keep this (supported)
this.turndown.addRule('bold', {
filter: ['strong', 'b'],
replacement: function (content) {
@@ -69,7 +114,7 @@ export class MarkdownConverter {
}
});
// Handle italic text in markdown
// Handle italic text in markdown - keep this (supported)
this.turndown.addRule('italic', {
filter: ['em', 'i'],
replacement: function (content) {
@@ -77,6 +122,42 @@ export class MarkdownConverter {
return '*' + content + '*';
}
});
// DISABLE unsupported features - convert to plain text
this.turndown.addRule('disableHeadings', {
filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
replacement: function (content) {
return content; // Just return text content, no # markup
}
});
this.turndown.addRule('disableLists', {
filter: ['ul', 'ol', 'li'],
replacement: function (content) {
return content; // Just return text content, no list markup
}
});
this.turndown.addRule('disableCode', {
filter: ['pre', 'code'],
replacement: function (content) {
return content; // Just return text content, no code markup
}
});
this.turndown.addRule('disableBlockquotes', {
filter: 'blockquote',
replacement: function (content) {
return content; // Just return text content, no > markup
}
});
this.turndown.addRule('disableHR', {
filter: 'hr',
replacement: function () {
return ''; // Remove horizontal rules entirely
}
});
}
/**