- Remove complex style preservation system from editor - Simplify markdown conversion back to straightforward approach - Remove StyleContext class and style-aware conversion methods - Switch content type from 'html' back to 'markdown' for consistency - Clean up editor workflow to focus on core markdown editing - Remove ~500 lines of unnecessary style complexity This completes the UI unification cleanup by removing the overly complex style preservation system that was making the editor harder to maintain.
288 lines
10 KiB
JavaScript
288 lines
10 KiB
JavaScript
/**
|
|
* Markdown conversion utilities using Marked and Turndown
|
|
*/
|
|
import { marked } from 'marked';
|
|
import TurndownService from 'turndown';
|
|
|
|
/**
|
|
* MarkdownConverter - Handles bidirectional HTML ↔ Markdown conversion
|
|
*/
|
|
export class MarkdownConverter {
|
|
constructor() {
|
|
this.initializeMarked();
|
|
this.initializeTurndown();
|
|
}
|
|
|
|
/**
|
|
* Configure marked for HTML output - MINIMAL MODE
|
|
* Only supports: **bold**, *italic*, and [links](url)
|
|
* Matches server-side goldmark configuration
|
|
*/
|
|
initializeMarked() {
|
|
marked.setOptions({
|
|
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: 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 - MINIMAL MODE
|
|
* Only supports: **bold**, *italic*, and [links](url)
|
|
* Matches server-side goldmark configuration
|
|
*/
|
|
initializeTurndown() {
|
|
this.turndown = new TurndownService({
|
|
// 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
|
|
});
|
|
|
|
// Add custom rules for better conversion
|
|
this.addTurndownRules();
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
this.turndown.addRule('paragraph', {
|
|
filter: 'p',
|
|
replacement: function (content) {
|
|
if (!content.trim()) return '';
|
|
return content.trim() + '\n\n';
|
|
}
|
|
});
|
|
|
|
// Handle bold text in markdown - keep this (supported)
|
|
this.turndown.addRule('bold', {
|
|
filter: ['strong', 'b'],
|
|
replacement: function (content) {
|
|
if (!content.trim()) return '';
|
|
return '**' + content + '**';
|
|
}
|
|
});
|
|
|
|
// Handle italic text in markdown - keep this (supported)
|
|
this.turndown.addRule('italic', {
|
|
filter: ['em', 'i'],
|
|
replacement: function (content) {
|
|
if (!content.trim()) return '';
|
|
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
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convert HTML to Markdown
|
|
* @param {string} html - HTML string to convert
|
|
* @returns {string} - Markdown string
|
|
*/
|
|
htmlToMarkdown(html) {
|
|
if (!html || html.trim() === '') {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
const markdown = this.turndown.turndown(html);
|
|
// Clean up and normalize newlines for proper paragraph separation
|
|
return markdown
|
|
.replace(/\n{3,}/g, '\n\n') // Replace 3+ newlines with 2
|
|
.replace(/^\n+|\n+$/g, '') // Remove leading/trailing newlines
|
|
.trim(); // Remove other whitespace
|
|
} catch (error) {
|
|
console.warn('HTML to Markdown conversion failed:', error);
|
|
// Fallback: extract text content
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = html;
|
|
return tempDiv.textContent || tempDiv.innerText || '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert Markdown to HTML
|
|
* @param {string} markdown - Markdown string to convert
|
|
* @returns {string} - HTML string
|
|
*/
|
|
markdownToHtml(markdown) {
|
|
if (!markdown || markdown.trim() === '') {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
const html = marked(markdown);
|
|
return html;
|
|
} catch (error) {
|
|
console.warn('Markdown to HTML conversion failed:', error);
|
|
// Fallback: convert line breaks to paragraphs
|
|
return markdown
|
|
.split(/\n\s*\n/)
|
|
.filter(p => p.trim())
|
|
.map(p => `<p>${p.trim()}</p>`)
|
|
.join('');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract HTML content from a group of elements
|
|
* @param {HTMLElement[]} elements - Array of DOM elements
|
|
* @returns {string} - Combined HTML content
|
|
*/
|
|
extractGroupHTML(elements) {
|
|
const htmlParts = [];
|
|
|
|
elements.forEach(element => {
|
|
// Wrap inner content in paragraph tags to preserve structure
|
|
const html = element.innerHTML.trim();
|
|
if (html) {
|
|
// If element is already a paragraph, use its outer HTML
|
|
if (element.tagName.toLowerCase() === 'p') {
|
|
htmlParts.push(element.outerHTML);
|
|
} else {
|
|
// Wrap in paragraph tags
|
|
htmlParts.push(`<p>${html}</p>`);
|
|
}
|
|
}
|
|
});
|
|
|
|
return htmlParts.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Convert HTML content from group elements to markdown
|
|
* @param {HTMLElement[]} elements - Array of DOM elements
|
|
* @returns {string} - Markdown representation
|
|
*/
|
|
extractGroupMarkdown(elements) {
|
|
const html = this.extractGroupHTML(elements);
|
|
const markdown = this.htmlToMarkdown(html);
|
|
return markdown;
|
|
}
|
|
|
|
/**
|
|
* Update group elements with markdown content
|
|
* @param {HTMLElement[]} elements - Array of DOM elements to update
|
|
* @param {string} markdown - Markdown content to render
|
|
*/
|
|
updateGroupElements(elements, markdown) {
|
|
const html = this.markdownToHtml(markdown);
|
|
|
|
// Split HTML into paragraphs
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = html;
|
|
|
|
const paragraphs = Array.from(tempDiv.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6'));
|
|
|
|
// Handle case where we have more/fewer paragraphs than elements
|
|
const maxCount = Math.max(elements.length, paragraphs.length);
|
|
|
|
for (let i = 0; i < maxCount; i++) {
|
|
if (i < elements.length && i < paragraphs.length) {
|
|
// Update existing element with corresponding paragraph
|
|
elements[i].innerHTML = paragraphs[i].innerHTML;
|
|
} else if (i < elements.length) {
|
|
// More elements than paragraphs - clear extra elements
|
|
elements[i].innerHTML = '';
|
|
} else if (i < paragraphs.length) {
|
|
// More paragraphs than elements - create new element
|
|
const newElement = document.createElement('p');
|
|
newElement.innerHTML = paragraphs[i].innerHTML;
|
|
|
|
// Insert after the last existing element
|
|
const lastElement = elements[elements.length - 1];
|
|
lastElement.parentNode.insertBefore(newElement, lastElement.nextSibling);
|
|
elements.push(newElement); // Add to our elements array for future updates
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const markdownConverter = new MarkdownConverter(); |