From 4de130e60879ad538248ddef734c1b618e8bf29b Mon Sep 17 00:00:00 2001 From: justanamelessguy Date: Mon, 1 Jun 2026 10:23:33 +0200 Subject: [PATCH] Add Markdown chapter content --- blog-post.html | 1 + css/style.css | 64 +++++++++++++++++++++ journey-edit.html | 1 + js/blog-post.js | 8 +-- js/journey-edit.js | 28 +++++---- js/journey-post.js | 1 - js/map-page.js | 18 +++--- js/markdown.js | 137 +++++++++++++++++++++++++++++++++++++++++++++ map-page.html | 17 ++---- 9 files changed, 236 insertions(+), 39 deletions(-) create mode 100644 js/markdown.js diff --git a/blog-post.html b/blog-post.html index e30835c..02f1288 100644 --- a/blog-post.html +++ b/blog-post.html @@ -311,6 +311,7 @@
+ diff --git a/css/style.css b/css/style.css index 34afb42..92fdb6e 100644 --- a/css/style.css +++ b/css/style.css @@ -194,6 +194,70 @@ textarea:focus { box-shadow: var(--shadow-3); } +.markdown-content { + color: var(--app-text) !important; + line-height: 1.65; +} + +.markdown-content > *:first-child { + margin-top: 0; +} + +.markdown-content > *:last-child { + margin-bottom: 0; +} + +.markdown-content h3, +.markdown-content h4, +.markdown-content h5 { + color: var(--app-text) !important; + margin: var(--size-3) 0 var(--size-2); +} + +.markdown-content p, +.markdown-content li { + color: var(--app-text) !important; +} + +.markdown-content a { + color: var(--app-link) !important; +} + +.markdown-content img { + max-width: 100%; + height: auto; + border-radius: 6px; + margin: var(--size-2) 0; +} + +.markdown-content code { + background: var(--surface-3); + border: 1px solid var(--app-border); + border-radius: 4px; + padding: 0.1rem 0.3rem; +} + +.markdown-content pre { + overflow-x: auto; + background: var(--surface-3); + border: 1px solid var(--app-border); + border-radius: 6px; + padding: var(--size-3); +} + +.markdown-preview { + margin-top: var(--size-2); + padding: var(--size-3); + border: 1px solid var(--app-border); + border-radius: 6px; + background: var(--app-panel-soft) !important; +} + +.markdown-empty { + color: var(--app-text-muted) !important; + font-style: italic; +} + /* Mobile-first approach */ .map-container { height: 100vh; diff --git a/journey-edit.html b/journey-edit.html index f0bb65d..6d48f9d 100644 --- a/journey-edit.html +++ b/journey-edit.html @@ -381,6 +381,7 @@
+ diff --git a/js/blog-post.js b/js/blog-post.js index 0ab2692..0bc599a 100644 --- a/js/blog-post.js +++ b/js/blog-post.js @@ -32,10 +32,9 @@ function renderJourney() { currentJourney.markers.forEach((marker, idx) => { const title = marker.title || `Chapter ${idx + 1}`; const date = marker.date ? `${new Date(marker.date).toLocaleDateString()}` : ''; - const content = marker.description ? escapeHtml(marker.description).replace(/\n/g, '
') : ''; + const content = marker.description ? renderMarkdown(marker.description) : ''; marker.images = getMarkerImages(marker); const images = getMarkerImagesHtml(marker.images, idx, canEdit); - const video = marker.videoUrl ? `` : ''; chaptersHtml += `
@@ -44,8 +43,7 @@ function renderJourney() {
${images} ${canEdit ? getImageToolsHtml(idx) : ''} - ${video} -
${content}
+
${content}
`; }); @@ -63,7 +61,7 @@ function renderJourney() { ${currentJourney.visibility === 'shared' && !isOwner ? `${canEdit ? 'Shared edit' : 'Shared'}` : ''} ${currentJourney.image ? `${currentJourney.title}` : ''} -
${escapeHtml(currentJourney.description).replace(/\n/g, '
')}
+
${renderMarkdown(currentJourney.description)}
${chaptersHtml} ${canEdit || isOwner ? `
diff --git a/js/journey-edit.js b/js/journey-edit.js index aea6971..736ff7d 100644 --- a/js/journey-edit.js +++ b/js/journey-edit.js @@ -144,6 +144,11 @@ function renderMarkerImages(marker, idx) { `; } +function renderMarkdownPreview(markdown) { + const html = renderMarkdown(markdown || ''); + return html || '

Nothing to preview yet.

'; +} + async function uploadMarkerImages(files) { if (!files.length) return []; @@ -186,17 +191,16 @@ function renderMarkers() {
- + +
+ ${renderMarkdownPreview(marker.description)} +
${renderMarkerImages(marker, idx)}
-
- - -
`).join(''); @@ -226,6 +230,8 @@ function renderMarkers() { textarea.addEventListener('input', (e) => { const idx = parseInt(textarea.dataset.index); markersData[idx].description = textarea.value; + const preview = document.querySelector(`.markdown-preview[data-preview-index="${idx}"]`); + if (preview) preview.innerHTML = renderMarkdownPreview(textarea.value); }); }); document.querySelectorAll('.marker-images').forEach(input => { @@ -249,12 +255,6 @@ function renderMarkers() { renderMarkers(); }); }); - document.querySelectorAll('.marker-video').forEach(input => { - input.addEventListener('input', (e) => { - const idx = parseInt(input.dataset.index); - markersData[idx].videoUrl = input.value; - }); - }); } // ==================== LOAD JOURNEY ==================== @@ -307,8 +307,7 @@ async function saveJourney(event) { title: m.title || '', date: m.date || '', description: m.description || '', - images: m.images || [], - videoUrl: m.videoUrl || '' + images: m.images || [] })); const payload = { title, description, markers }; @@ -356,8 +355,7 @@ function addMarker() { title: 'New Chapter', date: '', description: '', - images: [], - videoUrl: '' + images: [] }); renderMarkers(); } diff --git a/js/journey-post.js b/js/journey-post.js index 876e62d..818153e 100644 --- a/js/journey-post.js +++ b/js/journey-post.js @@ -12,7 +12,6 @@ function displayJourney(journey) {

${marker.content.date}

${marker.lngLat.lng.toFixed(4)}, ${marker.lngLat.lat.toFixed(4)}

${marker.content.text}

- ${marker.content.videoUrl ? `` : ''}
${marker.content.images.map(img => `Event image`).join('')}
diff --git a/js/map-page.js b/js/map-page.js index 2989fc1..e240c58 100644 --- a/js/map-page.js +++ b/js/map-page.js @@ -138,6 +138,12 @@ }); } + function updateMarkerMarkdownPreview(markdown) { + const preview = document.getElementById("marker-markdown-preview"); + if (!preview) return; + preview.innerHTML = renderMarkdown(markdown || "") || '

Nothing to preview yet.

'; + } + async function uploadMarkerImages(files) { if (!files.length) return []; @@ -166,8 +172,7 @@ content.date || ""; document.getElementById("marker-text").value = content.description || ""; - document.getElementById("video-url").value = - content.videoUrl || ""; + updateMarkerMarkdownPreview(content.description || ""); document.getElementById("marker-images").value = ""; renderMarkerImages(content.images || []); document.getElementById("marker-coords").value = @@ -189,7 +194,6 @@ const title = document.getElementById("marker-title").value || "Untitled"; const date = document.getElementById("marker-date").value; const description = document.getElementById("marker-text").value; - const videoUrl = document.getElementById("video-url").value; const imageInput = document.getElementById("marker-images"); const existingImages = activeMarker._content?.images || []; let images = existingImages; @@ -207,7 +211,7 @@ activeMarker.setPopupContent(`${title}`); // Store content for saving - activeMarker._content = { title, date, description, videoUrl, images }; + activeMarker._content = { title, date, description, images }; // Update the list item in the sidebar const latlng = activeMarker.getLatLng(); @@ -353,7 +357,6 @@ title: content.title || "", date: content.date || "", description: content.description || "", - videoUrl: content.videoUrl || "", images: content.images || [], }; }); @@ -436,7 +439,6 @@ title: m.title, date: m.date, description: m.description, - videoUrl: m.videoUrl, images: m.images || [], }); marker._content = { ...m }; @@ -469,7 +471,6 @@ title: displayTitle, date: m.date, description: m.description, - videoUrl: m.videoUrl, images: m.images || [], }); marker._content = { @@ -652,6 +653,9 @@ setMode("create"); showToast("Click on the map to add a marker"); }); + document + .getElementById("marker-text") + .addEventListener("input", (e) => updateMarkerMarkdownPreview(e.target.value)); } // Expose functions globally (optional) diff --git a/js/markdown.js b/js/markdown.js new file mode 100644 index 0000000..5c107d2 --- /dev/null +++ b/js/markdown.js @@ -0,0 +1,137 @@ +(function () { + const tokenPattern = /%%MDTOKEN(\d+)%%/g; + + function escapeHtml(value) { + return String(value || "").replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + })[char]); + } + + function sanitizeUrl(value) { + const url = String(value || "").trim().replace(/[\u0000-\u001f\u007f\s]+/g, ""); + if (/^(https?:\/\/|\/|\.\/|\.\.\/|uploads\/|mailto:|#)/i.test(url)) { + return escapeHtml(url); + } + return "#"; + } + + function storeToken(tokens, html) { + const token = `%%MDTOKEN${tokens.length}%%`; + tokens.push(html); + return token; + } + + function renderInline(markdown) { + const tokens = []; + let text = String(markdown || ""); + + text = text.replace(/`([^`]+)`/g, (_, code) => + storeToken(tokens, `${escapeHtml(code)}`), + ); + text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, url) => + storeToken( + tokens, + `${escapeHtml(alt)}`, + ), + ); + text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => + storeToken( + tokens, + `${escapeHtml(label)}`, + ), + ); + + text = escapeHtml(text) + .replace(/\*\*([^*]+)\*\*/g, "$1") + .replace(/__([^_]+)__/g, "$1") + .replace(/\*([^*]+)\*/g, "$1") + .replace(/_([^_]+)_/g, "$1"); + + return text.replace(tokenPattern, (_, index) => tokens[Number(index)] || ""); + } + + function renderList(lines) { + const items = lines + .map((line) => line.replace(/^\s*[-*]\s+/, "")) + .map((line) => `
  • ${renderInline(line)}
  • `) + .join(""); + return ``; + } + + function renderMarkdown(markdown) { + const lines = String(markdown || "").replace(/\r\n/g, "\n").split("\n"); + const blocks = []; + let paragraph = []; + let list = []; + let codeBlock = null; + + function flushParagraph() { + if (!paragraph.length) return; + blocks.push(`

    ${renderInline(paragraph.join(" "))}

    `); + paragraph = []; + } + + function flushList() { + if (!list.length) return; + blocks.push(renderList(list)); + list = []; + } + + lines.forEach((line) => { + if (line.trim().startsWith("```")) { + if (codeBlock) { + blocks.push(`
    ${escapeHtml(codeBlock.join("\n"))}
    `); + codeBlock = null; + } else { + flushParagraph(); + flushList(); + codeBlock = []; + } + return; + } + + if (codeBlock) { + codeBlock.push(line); + return; + } + + if (!line.trim()) { + flushParagraph(); + flushList(); + return; + } + + const heading = line.match(/^(#{1,3})\s+(.+)$/); + if (heading) { + flushParagraph(); + flushList(); + const level = heading[1].length + 2; + blocks.push(`${renderInline(heading[2])}`); + return; + } + + if (/^\s*[-*]\s+/.test(line)) { + flushParagraph(); + list.push(line); + return; + } + + flushList(); + paragraph.push(line.trim()); + }); + + flushParagraph(); + flushList(); + if (codeBlock) { + blocks.push(`
    ${escapeHtml(codeBlock.join("\n"))}
    `); + } + + return blocks.join(""); + } + + window.renderMarkdown = renderMarkdown; +})(); diff --git a/map-page.html b/map-page.html index d21c2e5..c4caf95 100644 --- a/map-page.html +++ b/map-page.html @@ -853,18 +853,12 @@ - -
    - - +
    @@ -913,6 +907,7 @@

    +