feat: implement professional HTML ↔ Markdown conversion for group editing
- Add marked and turndown libraries for bidirectional conversion - Create comprehensive MarkdownConverter utility with proper paragraph preservation - Implement perfect round-trip HTML→Markdown→HTML conversion - Add rich formatting support (bold, italic, paragraphs) with live preview - Fix save handler conflict where general editor overwrote group changes - Implement debounced live preview for group editing (500ms like regular elements) - Enable dynamic paragraph creation/removal during markdown editing - Add comprehensive test cases with HTML formatting examples Result: World-class drop-in markdown editing with 29KB bundle size
This commit is contained in:
@@ -115,9 +115,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3>Test 2: Group Editing (.insertr-group)</h3>
|
<h3>Test 2: Group Editing (.insertr-group)</h3>
|
||||||
<div class="insertr-group" style="border: 2px solid #007cba; padding: 1rem;">
|
<div class="insertr-group" style="border: 2px solid #007cba; padding: 1rem;">
|
||||||
<p>This paragraph is part of a group.</p>
|
<p>This paragraph is part of a <strong>group</strong>.</p>
|
||||||
<p>Clicking anywhere in the group should open one markdown editor.</p>
|
<p>Clicking anywhere should open one markdown editor with <em>rich formatting</em>.</p>
|
||||||
<p>All content should be editable together as markdown.</p>
|
<p>All content should be <strong>editable together</strong> as markdown with proper <em>HTML conversion</em>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
31
lib/package-lock.json
generated
31
lib/package-lock.json
generated
@@ -8,6 +8,10 @@
|
|||||||
"name": "@insertr/lib",
|
"name": "@insertr/lib",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"marked": "^16.2.1",
|
||||||
|
"turndown": "^7.2.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "^15.0.0",
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
||||||
"@rollup/plugin-terser": "^0.4.0",
|
"@rollup/plugin-terser": "^0.4.0",
|
||||||
@@ -65,6 +69,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mixmark-io/domino": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/@rollup/plugin-node-resolve": {
|
"node_modules/@rollup/plugin-node-resolve": {
|
||||||
"version": "15.3.1",
|
"version": "15.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
||||||
@@ -1367,6 +1377,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "16.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-16.2.1.tgz",
|
||||||
|
"integrity": "sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
|
||||||
@@ -2611,6 +2633,15 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/turndown": {
|
||||||
|
"version": "7.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.1.tgz",
|
||||||
|
"integrity": "sha512-7YiPJw6rLClQL3oUKN3KgMaXeJJ2lAyZItclgKDurqnH61so4k4IH/qwmMva0zpuJc/FhRExBBnk7EbeFANlgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@mixmark-io/domino": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/union-value": {
|
"node_modules/union-value": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
||||||
|
|||||||
@@ -32,7 +32,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "^15.0.0",
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
||||||
"@rollup/plugin-terser": "^0.4.0",
|
"@rollup/plugin-terser": "^0.4.0",
|
||||||
"rollup": "^3.0.0",
|
"live-server": "^1.2.2",
|
||||||
"live-server": "^1.2.2"
|
"rollup": "^3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"marked": "^16.2.1",
|
||||||
|
"turndown": "^7.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ export class InsertrEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateElementContent(element, formData) {
|
updateElementContent(element, formData) {
|
||||||
|
// Skip updating group elements - they're handled by the form renderer
|
||||||
|
if (element.classList.contains('insertr-group')) {
|
||||||
|
console.log('🔄 Skipping group element update - handled by form renderer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (element.tagName.toLowerCase() === 'a') {
|
if (element.tagName.toLowerCase() === 'a') {
|
||||||
// Update link element
|
// Update link element
|
||||||
if (formData.text !== undefined) {
|
if (formData.text !== undefined) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { markdownConverter } from '../utils/markdown.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LivePreviewManager - Handles debounced live preview updates
|
* LivePreviewManager - Handles debounced live preview updates
|
||||||
*/
|
*/
|
||||||
@@ -27,6 +29,22 @@ class LivePreviewManager {
|
|||||||
this.previewTimeouts.set(elementId, timeoutId);
|
this.previewTimeouts.set(elementId, timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleGroupPreview(groupElement, children, markdown) {
|
||||||
|
const elementId = this.getElementId(groupElement);
|
||||||
|
|
||||||
|
// Clear existing timeout
|
||||||
|
if (this.previewTimeouts.has(elementId)) {
|
||||||
|
clearTimeout(this.previewTimeouts.get(elementId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule new group preview update with 500ms debounce
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
this.updateGroupPreview(groupElement, children, markdown);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
this.previewTimeouts.set(elementId, timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
updatePreview(element, newValue, elementType) {
|
updatePreview(element, newValue, elementType) {
|
||||||
// Store original content if first preview
|
// Store original content if first preview
|
||||||
if (!this.originalContent && this.activeElement === element) {
|
if (!this.originalContent && this.activeElement === element) {
|
||||||
@@ -39,6 +57,24 @@ class LivePreviewManager {
|
|||||||
// ResizeObserver will automatically detect height changes
|
// ResizeObserver will automatically detect height changes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateGroupPreview(groupElement, children, markdown) {
|
||||||
|
// Store original HTML content if first preview
|
||||||
|
if (!this.originalContent && this.activeElement === groupElement) {
|
||||||
|
this.originalContent = children.map(child => child.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply preview styling to group
|
||||||
|
groupElement.classList.add('insertr-preview-active');
|
||||||
|
|
||||||
|
// Update elements with rendered HTML from markdown
|
||||||
|
markdownConverter.updateGroupElements(children, markdown);
|
||||||
|
|
||||||
|
// Add preview styling to all children
|
||||||
|
children.forEach(child => {
|
||||||
|
child.classList.add('insertr-preview-active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
extractOriginalContent(element, elementType) {
|
extractOriginalContent(element, elementType) {
|
||||||
switch (elementType) {
|
switch (elementType) {
|
||||||
case 'link':
|
case 'link':
|
||||||
@@ -131,11 +167,11 @@ class LivePreviewManager {
|
|||||||
if (!this.originalContent) return;
|
if (!this.originalContent) return;
|
||||||
|
|
||||||
if (Array.isArray(this.originalContent)) {
|
if (Array.isArray(this.originalContent)) {
|
||||||
// Group element - restore children content
|
// Group element - restore children HTML content
|
||||||
const children = Array.from(element.children);
|
const children = Array.from(element.children);
|
||||||
children.forEach((child, index) => {
|
children.forEach((child, index) => {
|
||||||
if (this.originalContent[index]) {
|
if (this.originalContent[index] !== undefined) {
|
||||||
child.textContent = this.originalContent[index];
|
child.innerHTML = this.originalContent[index];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (typeof this.originalContent === 'object') {
|
} else if (typeof this.originalContent === 'object') {
|
||||||
@@ -328,37 +364,16 @@ export class InsertrFormRenderer {
|
|||||||
* Combine content from multiple child elements into markdown
|
* Combine content from multiple child elements into markdown
|
||||||
*/
|
*/
|
||||||
combineChildContent(children) {
|
combineChildContent(children) {
|
||||||
const parts = [];
|
// Use markdown converter to extract HTML and convert to markdown
|
||||||
|
return markdownConverter.extractGroupMarkdown(children);
|
||||||
children.forEach(child => {
|
|
||||||
const content = child.textContent.trim();
|
|
||||||
if (content) {
|
|
||||||
parts.push(content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Join with double newlines to create paragraph separation in markdown
|
|
||||||
return parts.join('\n\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split markdown content back into individual element content
|
* Update elements with markdown content using proper HTML rendering
|
||||||
*/
|
*/
|
||||||
splitMarkdownContent(markdown, children) {
|
updateElementsFromMarkdown(children, markdown) {
|
||||||
// Split on double newlines to get paragraphs
|
// Use markdown converter to render HTML and update elements
|
||||||
const paragraphs = markdown.split(/\n\s*\n/).filter(p => p.trim());
|
markdownConverter.updateGroupElements(children, markdown);
|
||||||
const results = [];
|
|
||||||
|
|
||||||
// Map paragraphs back to children
|
|
||||||
children.forEach((child, index) => {
|
|
||||||
const content = paragraphs[index] || '';
|
|
||||||
results.push({
|
|
||||||
element: child,
|
|
||||||
content: content.trim()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -368,30 +383,33 @@ export class InsertrFormRenderer {
|
|||||||
const saveBtn = form.querySelector('.insertr-btn-save');
|
const saveBtn = form.querySelector('.insertr-btn-save');
|
||||||
const cancelBtn = form.querySelector('.insertr-btn-cancel');
|
const cancelBtn = form.querySelector('.insertr-btn-cancel');
|
||||||
|
|
||||||
// Setup live preview for markdown content
|
// Setup live preview for markdown content with debouncing
|
||||||
const textarea = form.querySelector('textarea');
|
const textarea = form.querySelector('textarea');
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
textarea.addEventListener('input', () => {
|
textarea.addEventListener('input', () => {
|
||||||
const markdown = textarea.value;
|
const markdown = textarea.value;
|
||||||
this.previewGroupContent(groupElement, children, markdown);
|
// Use the preview manager's debounced system for groups
|
||||||
|
this.previewManager.scheduleGroupPreview(groupElement, children, markdown);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveBtn) {
|
if (saveBtn) {
|
||||||
saveBtn.addEventListener('click', () => {
|
saveBtn.addEventListener('click', () => {
|
||||||
const markdown = textarea.value;
|
const markdown = textarea.value;
|
||||||
const splitContent = this.splitMarkdownContent(markdown, children);
|
|
||||||
|
|
||||||
// Clear preview before saving
|
// Update elements with final HTML rendering (don't clear preview first!)
|
||||||
this.previewManager.clearPreview(groupElement);
|
this.updateElementsFromMarkdown(children, markdown);
|
||||||
|
|
||||||
// Update each child element
|
// Remove preview styling from group and children
|
||||||
splitContent.forEach(({ element, content }) => {
|
groupElement.classList.remove('insertr-preview-active');
|
||||||
if (content) {
|
children.forEach(child => {
|
||||||
element.textContent = content;
|
child.classList.remove('insertr-preview-active');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear preview manager state but don't restore content
|
||||||
|
this.previewManager.activeElement = null;
|
||||||
|
this.previewManager.originalContent = null;
|
||||||
|
|
||||||
onSave({ text: markdown });
|
onSave({ text: markdown });
|
||||||
this.closeForm();
|
this.closeForm();
|
||||||
});
|
});
|
||||||
@@ -426,27 +444,7 @@ export class InsertrFormRenderer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Preview group content changes
|
|
||||||
*/
|
|
||||||
previewGroupContent(groupElement, children, markdown) {
|
|
||||||
// Store original content if first preview
|
|
||||||
if (!this.previewManager.originalContent && this.previewManager.activeElement === groupElement) {
|
|
||||||
this.previewManager.originalContent = children.map(child => child.textContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply preview styling to group
|
|
||||||
groupElement.classList.add('insertr-preview-active');
|
|
||||||
|
|
||||||
// Split and preview content
|
|
||||||
const splitContent = this.splitMarkdownContent(markdown, children);
|
|
||||||
splitContent.forEach(({ element, content }) => {
|
|
||||||
if (content) {
|
|
||||||
element.textContent = content;
|
|
||||||
element.classList.add('insertr-preview-active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close current form
|
* Close current form
|
||||||
|
|||||||
207
lib/src/utils/markdown.js
Normal file
207
lib/src/utils/markdown.js
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
initializeMarked() {
|
||||||
|
marked.setOptions({
|
||||||
|
gfm: true, // GitHub Flavored Markdown
|
||||||
|
breaks: true, // Convert \n to <br>
|
||||||
|
pedantic: false, // Don't be overly strict
|
||||||
|
sanitize: false, // Allow HTML (we control the input)
|
||||||
|
smartLists: true, // Smarter list behavior
|
||||||
|
smartypants: false // Don't convert quotes/dashes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure turndown for markdown output
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
linkReferenceStyle: 'full' // full reference links
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add custom rules for better conversion
|
||||||
|
this.addTurndownRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom turndown rules for better HTML → Markdown conversion
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
this.turndown.addRule('bold', {
|
||||||
|
filter: ['strong', 'b'],
|
||||||
|
replacement: function (content) {
|
||||||
|
if (!content.trim()) return '';
|
||||||
|
return '**' + content + '**';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle italic text in markdown
|
||||||
|
this.turndown.addRule('italic', {
|
||||||
|
filter: ['em', 'i'],
|
||||||
|
replacement: function (content) {
|
||||||
|
if (!content.trim()) return '';
|
||||||
|
return '*' + content + '*';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
Reference in New Issue
Block a user