diff --git a/src/css/editor.css b/src/css/editor.css index 024b468..58734e8 100644 --- a/src/css/editor.css +++ b/src/css/editor.css @@ -74,10 +74,24 @@ body.dragging .container .separator { display: flex; padding: .5rem; background: #ddd; + align-items: center; +} +.container .editor-header .status { + padding-right: .5rem; + display: none; +} +.container.status-loading .editor-header .status-loading { + display: block; +} +.container.status-saved .editor-header .status-saved { + display: block; } +.container.status-changed .editor-header .status-changed { + display: block; +} + .container .editor-header input[name="filename"] { border: 0; - width: 100%; display: block; background-color: transparent; color: var(--text-secondary-color); @@ -90,11 +104,18 @@ body.dragging .container .separator { background: var(--accent); color: #fff; border-radius: .25rem; + margin-left: auto; +} +.container .editor-header button[name="save"] i { + display: none; } .container .editor-header button[name="save"]:disabled { background: rgba(0,0,0,0.3); cursor: not-allowed; } +.container .editor-header button[name="save"].saving i { + display: inline-block; +} .container .editor-container { width: 50%; @@ -135,6 +156,9 @@ body.dragging .container .separator { flex-grow: 1; color: var(--text-color); background-color: var(--background-color); + + --token-color: #999; + } .container .editor-container pre.contentPre #content { outline: 0; @@ -199,16 +223,37 @@ body.dragging .container .preview-container .drag-overlay { } .mdAsterisk::after { content: "*"; - color: var(--text-secondary-color); + color: var(--token-color); } .mdUnderscore::after { content: "_"; - color: var(--text-secondary-color); + color: var(--token-color); } .mdHashmark::after { content: "#"; - color: var(--text-secondary-color); + color: var(--token-color); } .mdHtml { color: #fe7b00; +} +.mdQuote { + color: var(--text-secondary-color); +} +.mdGreaterThan::after { + content: ">"; + color: var(--token-color); +} +.mdDash::after { + content: "-"; + color: var(--token-color); +} +.mdListBullet { + color: var(--token-color); +} +.mdList { + color: darkgreen; +} +.mdEqual::after { + content: "="; + color: var(--token-color); } \ No newline at end of file diff --git a/src/ejs/editor.ejs b/src/ejs/editor.ejs index 34011d6..71c3cd4 100644 --- a/src/ejs/editor.ejs +++ b/src/ejs/editor.ejs @@ -27,6 +27,9 @@
+ + + diff --git a/src/js/client/editor.js b/src/js/client/editor.js index 0f21435..516fdc0 100644 --- a/src/js/client/editor.js +++ b/src/js/client/editor.js @@ -3,30 +3,59 @@ import { calculateReadingTime, countWords } from "../server/shared"; import "../../css/editor.css"; +let savedContent = null; +let isLoading = 0; +const updateStatus = () => { + const markdownText = document.getElementById("content").innerText; + + let status = "saved"; + if (isLoading > 0) { + status = "loading"; + } else if (savedContent !== markdownText) { + status = "changed"; + } else { + status = "saved"; + } + + document.querySelector(".container").className = document.querySelector(".container").className.replaceAll(/( status-loading| status-saved| status-changed)/gm, ""); + document.querySelector(".container").className += ` status-${status}`; + + return status; +}; + let iframeId = (new Date()).getTime(); let changeThrottle = null; const changeHandler = (event) => { + let markdownText = document.getElementById("content").innerText; - const savedContent = document.getElementById("savedContent").innerText; + if (!savedContent) savedContent = markdownText; - let markdownText = document.getElementById("content").innerText; - - if (savedContent !== markdownText) { - document.querySelector("form.editor-container button[name=save]").removeAttribute("disabled"); - } else { + const status = updateStatus(); + + if (status === "loading" || status === "saved") { document.querySelector("form.editor-container button[name=save]").setAttribute("disabled", true); + } else { + document.querySelector("form.editor-container button[name=save]").removeAttribute("disabled"); } const markupMap = { "*": "", "_": "", "#": "", + ">": "", + "-": "", + "=": "", + "list_bullet": "%%", "heading_open": "", "heading_close": "", "em_open": "", "em_close": "", "strong_open": "", "strong_close": "", + "quote_open": "", + "quote_close": "", + "list_open": "", + "list_close": "", }; let frontMatter = ''; @@ -41,25 +70,55 @@ const changeHandler = (event) => { const readingTime = calculateReadingTime(markdownText); document.querySelector(".editor-footer").innerHTML = `${wordCount} words | ${readingTime} minute${readingTime > 1 ? "s" : ""}`; - const mdHeadingMatch = markdownText.matchAll(/^(#+)(\s+.*)/gm); + const mdHeadingMatch = markdownText.matchAll(/^(#{1,6})(\s+.*)/gm); if (mdHeadingMatch) [...mdHeadingMatch].forEach(match => { const markup = match[1].split("").map((m) => markupMap[m]).join(""); markdownText = markdownText.replace(match[0], `${markupMap.heading_open}${markup}${match[2]}${markupMap.heading_close}`); }); + + const mdHeadingMatch2 = markdownText.matchAll(/^(.*\n)(-+\n|=+\n)/gm); + if (mdHeadingMatch2) [...mdHeadingMatch2].forEach(match => { + const markup = match[2].replaceAll("=", markupMap["="]).replaceAll("-", markupMap["-"]); + markdownText = markdownText.replace(match[0], `${markupMap.heading_open}${match[1]}${markup}${markupMap.heading_close}`); + }); + const mdLinkMatch = markdownText.matchAll(/!?\[[^\]]+?\]\([^\s]+?\)/gm); if (mdLinkMatch) [...mdLinkMatch].forEach(match => { markdownText = markdownText.replaceAll(match[0], `${match[0]}`); }); - const mdStrongMatch = markdownText.matchAll(/(\*\*|__)(.+?)\1/gm); + + const mdQuoteMatch = markdownText.matchAll(/^([ \t]*)(>(\s*>)*)(.*?\n\n)/gms); + if (mdQuoteMatch) [...mdQuoteMatch].forEach(match => { + const markup = match[2].replaceAll(">", markupMap['>']); + markdownText = markdownText.replace(match[0], `${match[1]}${markupMap.quote_open}${markup}${match[4]}${markupMap.quote_close}`); + }); + + const listMatch = (markdownText) => { + const mdListMatch = markdownText.matchAll(/^([ \t]*)(\*|-|\d+\.)(\s+.*?\n\n)/gms); + if (mdListMatch) [...mdListMatch].forEach(match => { + let content = match[3]; + if (content.match(/^([ \t]*)(\*|-|\d+\.)(\s+.*?\n\n)/ms)) content = listMatch(content); + const markup = markupMap[match[2]] ? markupMap[match[2]] : markupMap.list_bullet.replace("%%", match[2]); + markdownText = markdownText.replace(match[0], `${match[1]}${markupMap.list_open}${markup}${content}${markupMap.list_close}`); + }); + return markdownText; + } + markdownText = listMatch(markdownText); + + const mdStrongMatch = markdownText.matchAll(/(\*\*|__)(.+?)\1/gms); if (mdStrongMatch) [...mdStrongMatch].forEach(match => { + if (match[2].match(/\n\n/)) return; const markup = match[1].split("").map(m => markupMap[m]).join(""); markdownText = markdownText.replace(match[0], `${markupMap.strong_open}${markup}${match[2]}${markup}${markupMap.strong_close}`); }); - const mdEmMatch = markdownText.matchAll(/(\*|_)(.+?)\1/gm); + + const mdEmMatch = markdownText.matchAll(/(\*|_)(.+?)\1/gms); if (mdEmMatch) [...mdEmMatch].forEach(match => { + if (match[2].match(/\n\n/)) return; const markup = match[1].split("").map(m => markupMap[m]).join(""); markdownText = markdownText.replace(match[0], `${markupMap.em_open}${markup}${match[2]}${markup}${markupMap.em_close}`); }); + const mdHtmlMatch = markdownText.matchAll(/<[a-z/].*?>/gm); if (mdHtmlMatch) [...mdHtmlMatch].forEach(match => { markdownText = markdownText.replace(match[0], `${match[0]}`); @@ -73,6 +132,9 @@ const changeHandler = (event) => { if (document.querySelector("form.editor-container input[name=content]").value !== markdownText) { document.querySelector("form.editor-container input[name=content]").value = markdownText; + isLoading++; + updateStatus(); + const newIframe = document.createElement("iframe"); newIframe.setAttribute("name", `temp_iframe_${iframeId}`); document.querySelector(".preview-container").appendChild(newIframe); @@ -82,7 +144,14 @@ const changeHandler = (event) => { if (iframe.getAttribute("name") < event.target.getAttribute("name")) { iframe.remove(); } - }) + }); + + isLoading--; + updateStatus(); + + }); + newIframe.addEventListener("error", (event) => { + console.log(error); }); document.querySelector("form.editor-container").setAttribute("target", `temp_iframe_${iframeId}`); @@ -97,7 +166,8 @@ changeHandler(); const submitHandler = (event) => { if (event.submitter.name === "save") { - document.getElementById("savedContent").innerHTML = event.target.content.value; + savedContent = event.target.content.value; + changeHandler(); } }; diff --git a/src/js/server/common.js b/src/js/server/common.js index 57babcf..d2b123e 100644 --- a/src/js/server/common.js +++ b/src/js/server/common.js @@ -42,11 +42,27 @@ const renderHtml = async (config, templateName, basePath, skipDraft=false) => { if (blogPage.collection) { context.pages = context.blogPages.filter(bP => bP.collection === blogPage.collection).sort(BlogPage.compare); + + let previous; + let current; + let next; + context.pages.forEach((bP) => { + if (bP.path === blogPage.path) { + current = bP; + } else if (current && !next) { + next = bP; + blogPage.next = next.path; + } else if (!current) { + previous = bP; + blogPage.previous = previous.path; + } + }); } context = {...context, ...blogPage}; templateName = "/_blog_post"; } catch (error) { - // console.log(error); + // "ENOENT: no such file or directory" is expected when the page has no markdown file + if (!error.message.match(/ENOENT: no such file or directory/)) throw error; } const templatePath = `${config.templateDir}${templateName}.ejs`; diff --git a/src/md/blog/elden-ring-part-2.md b/src/md/blog/elden-ring-part-2.md index b27d465..bf2b06a 100644 --- a/src/md/blog/elden-ring-part-2.md +++ b/src/md/blog/elden-ring-part-2.md @@ -5,8 +5,6 @@ author: aorcsik published_at: 2024.07.11 tags: - elden-ring -previous: /blog/elden-ring.html -next: /blog/elden-ring-part-3.html collection: elden-ring --- diff --git a/src/md/blog/elden-ring-part-3.md b/src/md/blog/elden-ring-part-3.md index 5cc1d40..69fadf5 100644 --- a/src/md/blog/elden-ring-part-3.md +++ b/src/md/blog/elden-ring-part-3.md @@ -5,10 +5,8 @@ author: aorcsik published_at: 2024.07.16 tags: - elden-ring -previous: /blog/elden-ring-part-2.html -next: /blog/elden-ring-part-4.html -draft: true collection: elden-ring +draft: true --- ## I'm not strong enough diff --git a/src/md/blog/elden-ring.md b/src/md/blog/elden-ring.md index bb24f39..d194657 100644 --- a/src/md/blog/elden-ring.md +++ b/src/md/blog/elden-ring.md @@ -5,12 +5,10 @@ author: aorcsik published_at: 2024.07.08 tags: - elden-ring -next: /blog/elden-ring-part-2.html collection: elden-ring - --- -## First Steps +## First Stepss ### Adventures in Elden Ring - Part 1