Fix spacing on MD upload
This commit is contained in:
+113
-38
@@ -135,58 +135,133 @@ function generateHeadingId(text) {
|
||||
|
||||
/**
|
||||
* Convert markdown to HTML with enhanced formatting
|
||||
* - Preserves extra blank lines between paragraphs as visible gaps
|
||||
* (each extra blank line becomes <p class="md-gap"> </p>)
|
||||
* @param {string} markdown - Raw markdown content
|
||||
* @returns {string} HTML content
|
||||
*/
|
||||
function markdownToHtml(markdown) {
|
||||
if (!markdown) return "";
|
||||
|
||||
// Normalize line endings
|
||||
const GAP_TOKEN = "<GAP/>";
|
||||
const src = String(markdown).replace(/\r\n?/g, "\n");
|
||||
|
||||
// For 3+ consecutive newlines, keep 2 for the paragraph break and
|
||||
// emit a counted gap token for additional blank lines to preserve spacing.
|
||||
const withGaps = src.replace(/\n{3,}/g, (m) => {
|
||||
const extra = m.length - 2;
|
||||
return `\n\n<GAP:${extra}/>`;
|
||||
});
|
||||
|
||||
return (
|
||||
markdown
|
||||
withGaps
|
||||
// Headers with IDs
|
||||
.replace(/^### (.*$)/gim, (match, text) => {
|
||||
const id = generateHeadingId(text);
|
||||
return `<h3 id="${id}">${text}</h3>`;
|
||||
})
|
||||
.replace(/^## (.*$)/gim, (match, text) => {
|
||||
const id = generateHeadingId(text);
|
||||
return `<h2 id="${id}">${text}</h2>`;
|
||||
})
|
||||
.replace(/^# (.*$)/gim, (match, text) => {
|
||||
const id = generateHeadingId(text);
|
||||
return `<h1 id="${id}">${text}</h1>`;
|
||||
})
|
||||
// Bold and italic
|
||||
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||
// Code blocks
|
||||
.replace(
|
||||
/^###### (.*$)/gim,
|
||||
(m, t) => `<h6 id="${generateHeadingId(t)}">${t}</h6>`
|
||||
)
|
||||
.replace(
|
||||
/^##### (.*$)/gim,
|
||||
(m, t) => `<h5 id="${generateHeadingId(t)}">${t}</h5>`
|
||||
)
|
||||
.replace(
|
||||
/^#### (.*$)/gim,
|
||||
(m, t) => `<h4 id="${generateHeadingId(t)}">${t}</h4>`
|
||||
)
|
||||
.replace(
|
||||
/^### (.*$)/gim,
|
||||
(m, t) => `<h3 id="${generateHeadingId(t)}">${t}</h3>`
|
||||
)
|
||||
.replace(
|
||||
/^## (.*$)/gim,
|
||||
(m, t) => `<h2 id="${generateHeadingId(t)}">${t}</h2>`
|
||||
)
|
||||
.replace(
|
||||
/^# (.*$)/gim,
|
||||
(m, t) => `<h1 id="${generateHeadingId(t)}">${t}</h1>`
|
||||
)
|
||||
|
||||
// Code fences (block) and inline code
|
||||
.replace(
|
||||
/```(\w+)?\n([\s\S]*?)\n```/g,
|
||||
'<pre><code class="language-$1">$2</code></pre>'
|
||||
(m, lang = "", code) =>
|
||||
`<pre><code class="language-${lang}">${code}</code></pre>`
|
||||
)
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
// Links
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
||||
// Lists
|
||||
.replace(/^\* (.*$)/gim, "<li>$1</li>")
|
||||
.replace(/^- (.*$)/gim, "<li>$1</li>")
|
||||
.replace(/(<li>.*<\/li>)/gim, "<ul>$1</ul>")
|
||||
|
||||
// Bold and italic (strong before em to avoid overlap issues)
|
||||
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/\*(.+?)\*/g, "<em>$1</em>")
|
||||
|
||||
// Links and images
|
||||
.replace(
|
||||
/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g,
|
||||
(m, alt, src, title = "") =>
|
||||
`<img alt="${alt}" src="${src}"${title ? ` title="${title}"` : ""}>`
|
||||
)
|
||||
.replace(
|
||||
/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g,
|
||||
(m, text, href, title = "") =>
|
||||
`<a href="${href}"${title ? ` title="${title}"` : ""}>${text}</a>`
|
||||
)
|
||||
|
||||
// Blockquotes
|
||||
.replace(/^> (.*$)/gim, "<blockquote><p>$1</p></blockquote>")
|
||||
// Horizontal rules
|
||||
.replace(/^---$/gm, "<hr>")
|
||||
.replace(/^\*\*\*$/gm, "<hr>")
|
||||
// Paragraphs
|
||||
.replace(/\n\n/g, "</p><p>")
|
||||
.replace(/^(?!<[h|u|li|blockquote|hr|pre])(.*$)/gim, "<p>$1</p>")
|
||||
// Clean up empty paragraphs and fix list wrapping
|
||||
.replace(/<p><\/p>/g, "")
|
||||
.replace(/<p>(.*?)<\/p>/g, (match, content) => {
|
||||
return content.trim() ? match : "";
|
||||
.replace(/^(>\s?.+)(\n(>\s?.+))*$/gim, (m) => {
|
||||
const inner = m.replace(/^>\s?/gm, "");
|
||||
return `<blockquote><p>${inner.replace(
|
||||
/\n{2,}/g,
|
||||
"</p><p>"
|
||||
)}</p></blockquote>`;
|
||||
})
|
||||
.replace(/<\/ul>\s*<ul>/g, "") // Merge consecutive ul elements
|
||||
.replace(/<ul>\s*<\/ul>/g, "")
|
||||
); // Remove empty ul elements
|
||||
|
||||
// Lists (ul/ol)
|
||||
.replace(/^(\s*[-*]\s.+(?:\n\s*[-*]\s.+)*)/gim, (m) => {
|
||||
const items = m
|
||||
.trim()
|
||||
.split(/\n/)
|
||||
.map((l) => l.replace(/^\s*[-*]\s+/, ""))
|
||||
.map((t) => `<li>${t}</li>`)
|
||||
.join("");
|
||||
return `<ul>${items}</ul>`;
|
||||
})
|
||||
.replace(/^(\s*\d+\.\s.+(?:\n\s*\d+\.\s.+)*)/gim, (m) => {
|
||||
const items = m
|
||||
.trim()
|
||||
.split(/\n/)
|
||||
.map((l) => l.replace(/^\s*\d+\.\s+/, ""))
|
||||
.map((t) => `<li>${t}</li>`)
|
||||
.join("");
|
||||
return `<ol>${items}</ol>`;
|
||||
})
|
||||
|
||||
// Horizontal rules
|
||||
.replace(/^\s*(?:-{3,}|\*{3,})\s*$/gm, "<hr>")
|
||||
|
||||
// Paragraphs:
|
||||
// 1) Convert double newlines to paragraph boundaries
|
||||
.replace(/\n\n/g, "</p><p>")
|
||||
// 2) Convert single line breaks to <br> tags within paragraphs
|
||||
.replace(/(?<!\n)\n(?!\n)/g, "<br>")
|
||||
// 3) Wrap remaining bare lines that are not already block-level elements.
|
||||
// (Also skip our GAP_TOKEN so we can turn it into gap paragraphs later.)
|
||||
.replace(
|
||||
/^(?!\s*<(h[1-6]|ul|ol|li|blockquote|hr|pre|code|table|img)\b)(?!\s*<\/)(?!\s*<GAP\/>)(.+)$/gim,
|
||||
"<p>$2</p>"
|
||||
)
|
||||
|
||||
// Clean up truly empty paragraphs but keep gap paragraphs
|
||||
.replace(/<p>\s*<\/p>/g, "")
|
||||
|
||||
// Turn counted GAP tokens into explicit, styleable gap elements
|
||||
.replace(
|
||||
/<GAP:(\d+)\/>/g,
|
||||
(m, n) =>
|
||||
`<div class="md-gap" style="--gap:${Number(
|
||||
n
|
||||
)}" aria-hidden="true"></div>`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user