From a5c7e4dff1a5780620f6eef0844549f825434a0a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 16:33:28 +0000 Subject: [PATCH] Fix quote context markdown rendering and add delete buttons to preview items - Parse quotation context field with marked.parseInline() so markdown links render properly instead of showing raw [text](url) syntax - Add delete (x) buttons to all non-entry items (links, quotes, TILs, notes) in the newsletter preview, allowing items to be removed before copying - Delete buttons only appear in the preview, not in the copied HTML - Refactor generateNewsletter() to separate header from content HTML for clean preview vs copy output https://claude.ai/code/session_01QLdsf5LqsMeViAvqp3TytQ --- blog-to-newsletter.html | 86 ++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/blog-to-newsletter.html b/blog-to-newsletter.html index c359553..e9babd3 100644 --- a/blog-to-newsletter.html +++ b/blog-to-newsletter.html @@ -298,6 +298,29 @@ .long-urls-warning .edit-url-btn:hover { background: #e0a800; } + .newsletter-item { + position: relative; + } + .newsletter-item .delete-item-btn { + position: absolute; + top: 0; + right: 0; + background: #dc3545; + color: white; + border: none; + border-radius: 50%; + width: 24px; + height: 24px; + font-size: 16px; + line-height: 22px; + text-align: center; + cursor: pointer; + opacity: 0.5; + transition: opacity 0.15s; + } + .newsletter-item .delete-item-btn:hover { + opacity: 1; + } @@ -677,7 +700,8 @@

Links sent in previous newsletters

const info = typeof e.json === 'string' ? JSON.parse(e.json) : e.json; const entry = { ...e }; const quotationHtml = marked.parse(info.quotation || ''); - entry.html = `

Quote ${info.created}

${quotationHtml}

${info.source}${info.context ? ', ' + info.context : ''}

`; + const contextHtml = info.context ? ', ' + marked.parseInline(info.context) : ''; + entry.html = `

Quote ${info.created}

${quotationHtml}

${info.source}${contextHtml}

`; return entry; } @@ -901,7 +925,7 @@

Links sent in previous newsletters

function generateNewsletter() { const idOrder = storyOrder.map(s => parseInt(s.split(':')[0], 10)); - let html = ''; + let headerHtml = ''; // Table of contents if (entries.length) { @@ -911,9 +935,9 @@

Links sent in previous newsletters

return indexA - indexB; }); - html += '

In this newsletter:

'; + headerHtml += '

In this newsletter:

'; } // Summary of extras @@ -931,11 +955,11 @@

Links sent in previous newsletters

extras.push(`${notes.length} note${notes.length > 1 ? 's' : ''}`); } if (extras.length) { - html += `

Plus ${extras.join(' and ')}

`; + headerHtml += `

Plus ${extras.join(' and ')}

`; } // Sponsor message - html += `

If you find this newsletter useful, please consider sponsoring me via GitHub. $10/month and higher sponsors get a monthly newsletter with my summary of the most important trends of the past 30 days - here are previews from October and November.

`; + headerHtml += `

If you find this newsletter useful, please consider sponsoring me via GitHub. $10/month and higher sponsors get a monthly newsletter with my summary of the most important trends of the past 30 days - here are previews from October and November.

`; // Content sorted by story order const sortedContent = [...content].sort((a, b) => { @@ -945,21 +969,53 @@

Links sent in previous newsletters

return indexA - indexB; }); - html += sortedContent.map(c => c.html + '
').join('\n'); - - // Apply URL replacements - for (const [oldUrl, newUrl] of urlReplacements) { - html = html.split(oldUrl).join(newUrl); + // Apply URL replacements helper + function applyReplacements(html) { + for (const [oldUrl, newUrl] of urlReplacements) { + html = html.split(oldUrl).join(newUrl); + } + return html; } - newsletterHTML = html; + // Clean newsletter HTML (for copying) + let html = headerHtml + sortedContent.map(c => c.html + '
').join('\n'); + newsletterHTML = applyReplacements(html); htmlLengthEl.textContent = `Length of HTML: ${newsletterHTML.length.toLocaleString()} characters`; - previewEl.innerHTML = html; + + // Preview HTML with delete buttons for non-entry items + const previewContentHtml = sortedContent.map(c => { + const itemHtml = applyReplacements(c.html); + if (c.type !== 'entry') { + return `
${itemHtml}

`; + } + return itemHtml + '
'; + }).join('\n'); + previewEl.innerHTML = applyReplacements(headerHtml) + previewContentHtml; // Check for long URLs and display warnings - checkLongUrls(html); + checkLongUrls(newsletterHTML); + } + + // Remove an item from the newsletter + function removeItem(type, id) { + content = content.filter(e => !(e.type === type && String(e.id) === String(id))); + blogmarks = blogmarks.filter(e => !(type === 'blogmark' && String(e.id) === String(id))); + quotations = quotations.filter(e => !(type === 'quotation' && String(e.id) === String(id))); + tils = tils.filter(e => !(type === 'til' && String(e.id) === String(id))); + notes = notes.filter(e => !(type === 'note' && String(e.id) === String(id))); + generateNewsletter(); } + // Event delegation for delete buttons in preview + previewEl.addEventListener('click', (e) => { + if (e.target.classList.contains('delete-item-btn')) { + const wrapper = e.target.closest('.newsletter-item'); + if (wrapper) { + removeItem(wrapper.dataset.type, wrapper.dataset.id); + } + } + }); + // Check for URLs longer than 200 characters function checkLongUrls(html) { const urlRegex = /href="(https?:\/\/[^"]+)"/g;