diff --git a/.eslintrc.json b/.eslintrc.json index 0e3ec36..92699ab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,6 +26,7 @@ "HTMLParagraphElement", "HTMLSpanElement", "Promise", + "Range", "URL", "Window" ] diff --git a/src/css/editor.css b/src/css/editor.css index 04aa21b..0f834a7 100644 --- a/src/css/editor.css +++ b/src/css/editor.css @@ -18,6 +18,7 @@ button, input, select, textarea { } pre, code, samp, kbd, var { font-family: var(--monospace-font); + font-variant-ligatures: none; } .container { @@ -26,14 +27,17 @@ pre, code, samp, kbd, var { height: 100vh; --accent: #0080b3; - --background-color: #f2f2f2; - --text-color: #000; - --text-secondary-color: #666; - --link-color: var(--accent); + --background-color: #181818; + --text-color: #CCCCCC; + --text-secondary-color: #9B9B9B; + --border-color: #2B2B2B; + + color: var(--text-color); + background-color: var(--background-color); } .container .separator { - background: #ddd; + background-color: var(--border-color); display: flex; align-items: center; justify-content: center; @@ -55,7 +59,7 @@ pre, code, samp, kbd, var { .container .separator:hover, body.dragging .container .separator { background-color: var(--accent); - color: #fff; + color: var(--text-color); } body.dragging, body.dragging .container .separator { @@ -67,14 +71,14 @@ body.dragging .container .separator { .container .editor-footer { font-size: 0.8rem; padding: .25rem .5rem; - background-color: #ddd; + border-top: 1px solid var(--border-color); } .container .editor-header { display: flex; padding: .5rem; - background: #ddd; align-items: center; + border-bottom: 1px solid var(--border-color); } .container .editor-header .status { padding-right: .5rem; @@ -102,7 +106,7 @@ body.dragging .container .separator { padding: .5rem 1rem; border: 0; background: var(--accent); - color: #fff; + color: var(--text-color); border-radius: .25rem; margin-left: auto; } @@ -123,6 +127,14 @@ body.dragging .container .separator { height: 100%; display: flex; flex-direction: column; + + --syntax-background: #1F1F1F; + --syntax-color: #CCCCCC; + --syntax-color-1: #569cd6; + --syntax-color-2: #CE9178; + --syntax-color-3: #6A9956; + --syntax-color-4: #9CDCFE; + --syntax-line-number-color: #6D7681; } .container .editor-container .editor-content { overflow: auto; @@ -130,10 +142,9 @@ body.dragging .container .separator { height: 100%; display: flex; flex-direction: column; -} -.container .editor-container .editor-content #savedContent { - display: none; + color: var(--syntax-color); + background-color: var(--syntax-background); } .container .editor-container pre { @@ -142,6 +153,8 @@ body.dragging .container .separator { padding: 1rem 1rem 50% 1rem; font-size: 0.8rem; } +.container .editor-container pre > div { +} .container .editor-container pre.contentPre { position: absolute; top: 0; @@ -149,19 +162,22 @@ body.dragging .container .separator { right: 0; bottom: 0; color: transparent; - /* color: rgba(0,0,0,.3); */ - caret-color: var(--text-color); +} +.container .editor-container pre.contentLinesPre { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; } .container .editor-container pre.contentStylePre { flex-grow: 1; - color: var(--text-color); - background-color: var(--background-color); - - --token-color: #999; - + color: var(--syntax-color); + /* background-color: var(--syntax-background); */ } .container .editor-container pre.contentPre #content { outline: 0; + caret-color: var(--syntax-color); /** * Prevent contenteditable to add p, div items on ENTER @@ -208,65 +224,122 @@ body.dragging .container .preview-container .drag-overlay { .mdFrontMatter { color: var(--text-secondary-color); - background: rgba(0,0,0,0.025); + /* background: rgba(255,255,255,.025); display: block; padding: 1rem 1rem .5rem 1rem; margin: -1rem -1rem -.5rem -1rem; - border-radius: 4px; -} -.mdEm { - font-style: italic; -} -.mdStrong { - font-weight: bold; -} -.mdLink { - color: var(--link-color); -} -.mdHeading { - font-weight: bold; + border-radius: 4px; */ } + +/* visible characters */ + .mdAsterisk::after { content: "*"; - color: var(--token-color); } .mdUnderscore::after { content: "_"; - color: var(--token-color); } .mdHashmark::after { content: "#"; - color: var(--token-color); -} -.mdHtml { - color: #fe7b00; -} -.mdQuote { - color: darkmagenta; } .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); } +.mdTilde::after { + content: "~"; +} +.mdBacktick::after { + content: "`"; +} + +/* invisible characters */ .mdNewLine::after { content: "\21B5"; color: var(--token-color); opacity: .3; } + +/* elements */ + +.mdHeading { + font-weight: bold; + color: var(--syntax-color-1); +} +.mdStrong { + font-weight: bold; + color: var(--syntax-color-1); +} +.mdEm { + font-style: italic; +} +.mdStrikethrough { + text-decoration: line-through; +} +.mdLink { + +} +.mdLink .mdLinkText { + color: var(--syntax-color-2); +} +.mdLink .mdLinkUrl { + text-decoration: underline; +} +.mdLink .mdLinkTitle { + color: var(--syntax-color-2); +} +.mdHtml { + color: var(--syntax-color-1); +} +.mdHtml .mdHtmlAttrName { + color: var(--syntax-color-4); +} +.mdHtml .mdHtmlAttrValue { + color: var(--syntax-color-2); +} +.mdCode { + color: var(--syntax-color-2); +} +.mdCodeBlock { + color: var(--syntax-color-2); +} +.mdQuote { + +} +.mdQuote .mdGreaterThan { + color: var(--syntax-color-3); +} +.mdList { + +} +.mdList .mdListBullet { + color: var(--syntax-color-1); +} .mdHr { -} \ No newline at end of file +} + + +.mdLine { + display: inline-flex; +} +.mdLine .mdLineNumber { + text-align: right; + flex-shrink: 0; + color: var(--syntax-line-number-color); +} +.mdLine.mdLineActive .mdLineNumber { + color: var(--syntax-color); +} +.mdLine .mdLineContent { + flex-grow: 1; + padding-left: 1ch; + color: transparent; +} + +/* --syntax-color-1 */ diff --git a/src/ejs/editor.ejs b/src/ejs/editor.ejs index 71c3cd4..14d5554 100644 --- a/src/ejs/editor.ejs +++ b/src/ejs/editor.ejs @@ -35,8 +35,8 @@
-
<%= context.rawContent %>
-
<%= context.rawContent %>
+
+
<%= context.rawContent %>
diff --git a/src/js/client/editor.js b/src/js/client/editor.js index 895e019..746fc5d 100644 --- a/src/js/client/editor.js +++ b/src/js/client/editor.js @@ -3,6 +3,25 @@ import { calculateReadingTime, countWords } from "../server/shared"; import "../../css/editor.css"; +const contentScrollHandler = () => { + const editorScrollValue = document.querySelector(".editor-content").scrollTop; + const editorContentHeight = document.querySelector(".editor-content pre").offsetHeight; + const editorContainerHeight = document.querySelector(".editor-content").offsetHeight; + + const readPercentage = getReadPercentage(editorScrollValue, editorContentHeight, editorContainerHeight); + + document.querySelectorAll("iframe").forEach((iframe) => { + /** @type {Window} */ + const iframeWindow = iframe.contentWindow || iframe; + + const iframeContainerHeight = iframeWindow.innerHeight; + const iframeContentHeight = iframeWindow.document.querySelector("body").clientHeight; + const iframeScrollValue = getScrollValue(readPercentage, iframeContentHeight, iframeContainerHeight); + + iframeWindow.scrollTo(0, iframeScrollValue); + }); +}; + let savedContent = null; let isLoading = 0; const updateStatus = () => { @@ -20,24 +39,85 @@ const updateStatus = () => { document.querySelector(".container").className = document.querySelector(".container").className.replaceAll(/( status-loading| status-saved| status-changed)/gm, ""); document.querySelector(".container").className += ` status-${status}`; + 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"); + } + return status; }; +/** + * + * @param {HTMLElement} editableContent + * @returns {Range} + */ +const getRange = (editableContent) => { + if (window.getSelection) { + const sel = window.getSelection(); + if (sel.rangeCount) { + const range = sel.getRangeAt(0); + if (range.commonAncestorContainer.parentNode == editableContent) { + return range; + } + } + } + return null; +} + let iframeId = (new Date()).getTime(); let changeThrottle = null; const changeHandler = (event) => { - let markdownText = document.getElementById("content").innerText; - if (!savedContent) savedContent = markdownText; - - const status = updateStatus(); + /* fix Grammarly blend mode issue with dark content */ + // document.querySelectorAll("grammarly-extension").forEach((gE) => { + // const styleNode = document.createElement("style"); + // styleNode.textContent = ` + // div[data-grammarly-part="highlight"] { + // mix-blend-mode: normal !important; + // } + // `; + // gE.shadowRoot.appendChild(styleNode); + // }); + + + const editableContent = document.getElementById("content"); + const styleContent = document.getElementById("contentStyle"); + const linesContent = document.getElementById("contentLines"); + + const editorInfo = { + line: 0, + column: 0, + wordCount: 0, + readingTime: 0, + }; - 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"); + let markdownText = editableContent.innerText; + + const selection = getRange(editableContent); + if (selection) { + let charCount = 0; + markdownText.split("\n").forEach((l, idx) => { + const line = `${l}\n`; + const lineStart = charCount; + const lineEnd = charCount + line.length; + if (selection.startOffset >= lineStart && selection.startOffset < lineEnd) { + // console.log("start:", idx, selection.startOffset - lineStart, line); + } + if (selection.endOffset >= lineStart && selection.endOffset < lineEnd) { + // console.log("end:", idx, selection.endOffset - lineStart, line); + editorInfo.line = idx + 1; + editorInfo.column = selection.endOffset - lineStart + 1; + } + charCount = lineEnd; + }); } + if (!savedContent) savedContent = markdownText; + + updateStatus(); + const markupMap = { "*": "", "_": "", @@ -45,29 +125,44 @@ const changeHandler = (event) => { ">": "", "-": "", "=": "", + "~": "", + "`": "", "newline": "", "heading": "%%", "em": "%%", "strong": "%%", + "strikethrough": "%%", "quote": "%%", "list": "%%", "list_bullet": "%%", - "link": "%%", + "link": "[%1](%2%3)", + "code": "%%", + "code_block": "%%", "html": "%%", "hr": "%%", }; - let frontMatter = ''; markdownText = markdownText.replaceAll("<", "<").replaceAll(">", ">"); + + const gutterSize = `${markdownText.split("\n").length}`.length; + linesContent.innerHTML = markdownText.split("\n").map((line, idx) => { + return `` + + `${idx + 1}` + + `${line}` + + ""; + }).slice(0, -1).join("\n") + "\n"; + styleContent.style.paddingLeft = `${gutterSize + 1}ch`; + editableContent.style.paddingLeft = `${gutterSize + 1}ch`; + + let frontMatter = ''; const mdFrontMatterMatch = markdownText.match(/---.*?---\n/ms); if (mdFrontMatterMatch) { frontMatter = `${mdFrontMatterMatch[0]}`; markdownText = markdownText.replace(mdFrontMatterMatch[0], ''); } - const wordCount = countWords(markdownText); - const readingTime = calculateReadingTime(markdownText); - document.querySelector(".editor-footer").innerHTML = `${wordCount} words | ${readingTime} minute${readingTime > 1 ? "s" : ""}`; + editorInfo.wordCount = countWords(markdownText); + editorInfo.readingTime = calculateReadingTime(markdownText); const mdHeadingMatch = markdownText.matchAll(/^(#{1,6})(\s+.*)/gm); if (mdHeadingMatch) [...mdHeadingMatch].forEach(match => { @@ -87,9 +182,27 @@ const changeHandler = (event) => { markdownText = markdownText.replace(match[0], markupMap.hr.replace("%%", `${markup}`)); }); - const mdLinkMatch = markdownText.matchAll(/!?\[[^\]]+?\]\([^\s]+?(\s+".*?")?\)/gm); + const mdCodeBlockMatch = markdownText.matchAll(/(```)(\s*[^\s]?)(.*?)\1\n/gms); + if (mdCodeBlockMatch) [...mdCodeBlockMatch].forEach(match => { + const markup = match[1].split("").map((m) => markupMap[m]).join(""); + console.log(match); + markdownText = markdownText.replaceAll(match[0], markupMap.code_block.replace("%%", `${markup}${match[2] || ''}${match[3]}${markup}`)); + }); + + const mdCodeMatch = markdownText.matchAll(/(`)(.+?)\1/gms); + if (mdCodeMatch) [...mdCodeMatch].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.code.replace("%%", `${markup}${match[2]}${markup}`)); + }); + + const mdLinkMatch = markdownText.matchAll(/!?\[([^\]]+?)\]\(([^\s]+?)(\s+".*?")?\)/gm); if (mdLinkMatch) [...mdLinkMatch].forEach(match => { - markdownText = markdownText.replaceAll(match[0], markupMap.link.replace("%%", match[0])); + markdownText = markdownText.replaceAll(match[0], markupMap.link + .replace("%1", match[1]) + .replace("%2", match[2]) + .replace("%3", match[3] || '') + ); }); const mdQuoteMatch = markdownText.matchAll(/^([ \t]*)(>(\s*>)*)(.*?\n\n)/gms); @@ -110,13 +223,20 @@ const changeHandler = (event) => { 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]); + const markup = markupMap.list_bullet.replace("%%", markupMap[match[2]] ? markupMap[match[2]] : match[2]); markdownText = markdownText.replace(match[0], `${match[1]}${markupMap.list.replace("%%", `${markup}${content}`)}`); }); return markdownText; } markdownText = listMatch(markdownText); + const mdStrikethroughMatch = markdownText.matchAll(/(~~)(.+?)\1/gms); + if (mdStrikethroughMatch) [...mdStrikethroughMatch].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.strikethrough.replace("%%", `${markup}${match[2]}${markup}`)); + }); + const mdStrongMatch = markdownText.matchAll(/(\*\*|__)(.+?)\1/gms); if (mdStrongMatch) [...mdStrongMatch].forEach(match => { if (match[2].match(/\n\n/)) return; @@ -133,12 +253,22 @@ const changeHandler = (event) => { const mdHtmlMatch = markdownText.matchAll(/<[\w/].*?>/gm); if (mdHtmlMatch) [...mdHtmlMatch].forEach(match => { - markdownText = markdownText.replace(match[0], markupMap.html.replace("%%", match[0])); + let content = match[0]; + const attributeMatch = match[0].matchAll(/([^\s]+=)((['"]).*?\3)/gm); + if (attributeMatch) [...attributeMatch].forEach((attrMatch) => { + content = content.replaceAll(attrMatch[0], `${attrMatch[1]}${attrMatch[2]}`); + }); + markdownText = markdownText.replace(match[0], markupMap.html.replace("%%", content)); }); markdownText = markdownText.replaceAll(/\n/gms, `${markupMap.newline}\n`); document.getElementById("contentStyle").innerHTML = frontMatter + markdownText; + document.querySelector(".editor-footer").innerHTML = ` + Ln ${editorInfo.line}, Col ${editorInfo.column} | + ${editorInfo.wordCount} words | + ${editorInfo.readingTime} minute${editorInfo.readingTime > 1 ? "s" : ""} + `; window.clearTimeout(changeThrottle); changeThrottle = window.setTimeout(() => { @@ -152,18 +282,20 @@ const changeHandler = (event) => { const newIframe = document.createElement("iframe"); newIframe.setAttribute("name", `temp_iframe_${iframeId}`); document.querySelector(".preview-container").appendChild(newIframe); - newIframe.addEventListener("load", (event) => { - event.target.style.opacity = 1; + window.iframeLoaded = () => { + const iframeName = document.querySelector("form.editor-container").getAttribute('target'); + document.querySelector(`iframe[name="${iframeName}"]`).style.opacity = 1; document.querySelectorAll("iframe").forEach(iframe => { - if (iframe.getAttribute("name") < event.target.getAttribute("name")) { + if (iframe.getAttribute("name") < iframeName) { iframe.remove(); } }); - + isLoading--; updateStatus(); + contentScrollHandler(); + }; - }); newIframe.addEventListener("error", (event) => { console.log(error); }); @@ -181,31 +313,16 @@ changeHandler(); const submitHandler = (event) => { if (event.submitter.name === "save") { savedContent = event.target.content.value; - changeHandler(); + // changeHandler(); } }; -const contentScrollHandler = () => { - const editorScrollValue = document.querySelector(".editor-content").scrollTop; - const editorContentHeight = document.querySelector(".editor-content pre").offsetHeight; - const editorContainerHeight = document.querySelector(".editor-content").offsetHeight; - - const readPercentage = getReadPercentage(editorScrollValue, editorContentHeight, editorContainerHeight); - - document.querySelectorAll("iframe").forEach((iframe) => { - /** @type {Window} */ - const iframeWindow = iframe.contentWindow || iframe; - - const iframeContainerHeight = iframeWindow.innerHeight; - const iframeContentHeight = iframeWindow.document.querySelector("body").clientHeight; - const iframeScrollValue = getScrollValue(readPercentage, iframeContentHeight, iframeContainerHeight); - - iframeWindow.scrollTo(0, iframeScrollValue); - }); +const saveHandéer = () => { + let markdownText = document.getElementById("content").innerText; + document.querySelector("form.editor-container input[name=content]").value = markdownText; + savedContent = markdownText; }; -window.contentScrollHandler = contentScrollHandler; - let separatorDragging = false; const separatorDragStartHandler = (event) => { @@ -228,17 +345,21 @@ const separatorDragStopHandler = (event) => { } }; -"keyup paste input".split(" ").forEach(eventType => document.getElementById("content").addEventListener(eventType, changeHandler)); +"focus keyup paste input click".split(" ").forEach(eventType => document.getElementById("content").addEventListener(eventType, changeHandler)); document.querySelector("form.editor-container").addEventListener("submit", submitHandler); +// document.querySelector("button[name=save]").addEventListener("click", saveHandéer); document.querySelector(".editor-content").addEventListener("scroll", contentScrollHandler); document.querySelector(".separator").addEventListener("mousedown", separatorDragStartHandler); window.addEventListener("mousemove", separatorDragHandler); window.addEventListener("mouseup", separatorDragStopHandler); +document.getElementById("content").focus(); + if (import.meta.webpackHot) { import.meta.webpackHot.dispose(() => { - "keyup paste input".split(" ").forEach(eventType => document.getElementById("content").removeEventListener(eventType, changeHandler)); + "focus keyup paste input click".split(" ").forEach(eventType => document.getElementById("content").removeEventListener(eventType, changeHandler)); document.querySelector("form.editor-container").removeEventListener("submit", submitHandler); + // document.querySelector("button[name=save]").removeEventListener("click", saveHandéer); document.querySelector(".editor-content").removeEventListener("scroll", contentScrollHandler); document.querySelector(".separator").removeEventListener("mousedown", separatorDragStartHandler); window.removeEventListener("mousemove", separatorDragHandler); diff --git a/src/js/client/preview.js b/src/js/client/preview.js index 6ba29fe..fb45da7 100644 --- a/src/js/client/preview.js +++ b/src/js/client/preview.js @@ -1,30 +1,6 @@ -// import { getReadPercentage, getScrollValue } from "./common"; - -// window.scrollTo(0, window.parent.iframeScrollY); -window.parent.contentScrollHandler(); - -// const scrollHandler = async (event) => { -// const scrollValue = window.scrollY; -// const contentHeight = document.querySelector('body').clientHeight; -// const containerHeight = window.innerHeight; - -// const readPercentage = getReadPercentage(scrollValue, contentHeight, containerHeight); - -// const parentContainerHeight = window.parent.document.querySelector(".editor-content").offsetHeight; -// const parentContentHeight = window.parent.document.querySelector(".editor-content pre").offsetHeight; -// const parentScrollValue = getScrollValue(readPercentage, parentContentHeight, parentContainerHeight); - -// console.log(parentScrollValue); - -// window.parent.document.querySelector(".editor-content").scrollTop = parentScrollValue; -// }; - -// window.addEventListener("scroll", scrollHandler); +window.parent.iframeLoaded(); if (import.meta.webpackHot) { - import.meta.webpackHot.dispose(() => { - // scrollHandler(); - // window.removeEventListener("scroll", scrollHandler); - }); + import.meta.webpackHot.dispose(() => {}); import.meta.webpackHot.accept(); } \ No newline at end of file diff --git a/src/md/blog/elden-ring-part-2.md b/src/md/blog/elden-ring-part-2.md index bf2b06a..7aa419e 100644 --- a/src/md/blog/elden-ring-part-2.md +++ b/src/md/blog/elden-ring-part-2.md @@ -10,7 +10,7 @@ collection: elden-ring ## Beyond Limgrave -### Adventures in Elden Ring - Part 2 +### Adventures in Elden Ring - Part 2
@@ -108,3 +108,4 @@ Past the shack, I found [Cuckoo's Evergaol](https://eldenring.wiki.fextralife.co [Sir Oakheart vs. Bold, Carian Knight](https://youtu.be/gmAtywJeJHs#embed) I followed the road, fought some agitated [Spirit Jellyfish](https://eldenring.wiki.fextralife.com/Spirit+Jellyfish), and found a [Jellyfish Shield](https://eldenring.wiki.fextralife.com/Jellyfish+Shield). I was walking around with a huge translucent blob on my back from then on. From here I didn't stop until [Caria Manor](https://eldenring.wiki.fextralife.com/Caria+Manor). There, I was dealt a bunch of [hands](https://eldenring.wiki.fextralife.com/Fingercreeper), that I had to deal with. These creatures frequently drop [Somber Smithing Stones](https://eldenring.wiki.fextralife.com/Somber+Smithing+Stones). This place was full of magic and dangerous enemies like [Raya Lucaria Soldier](https://eldenring.wiki.fextralife.com/Raya+Lucaria+Soldier) puppets, [Direwolves](https://eldenring.wiki.fextralife.com/Direwolf), and even a [Troll Knight](https://eldenring.wiki.fextralife.com/Troll+Knight), like the one who I fought in the Evergaol. I might not be strong enough for this place yet. *(At the time I didn't know I was just a few steps away from the boss, but leaving to get stronger was a good decision.)* +