Files
website/views/editor.ejs
2026-01-19 01:31:10 -05:00

257 lines
10 KiB
Plaintext
Executable File

<!DOCTYPE html>
<html lang="en">
<head>
<title>Oddbyte</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link id="favicon" rel="shortcut icon" type="image/png" href="data:image/png;base64,<%- faviconb64 %>">
<link rel="canonical" href="https://oddbyte.dev/"/>
<meta name="fediverse:creator" content="@oddbyte@mastodon.social">
<style>
<%- include('partials/style') %>
/* Begin per-page stylesheet */
#markdown-editor {
width: 95%;
height: 500px;
border: 1px solid;
border-radius: 4px;
padding: 15px;
font-family: 'Times New Roman', Times, serif;
resize: none;
background-color: #2d3339;
color: #f8f9fa;
border-color: #495057;
white-space: pre-line;
}
#editor-container {
width: 100%;
}
#html-preview {
width: 95%;
height: 500px;
border: none;
}
</style>
</head>
<body>
<%- include('partials/navbar') %>
<div id="container_top">
<div id="container_main">
<div id="container_thing">
<main>
<a href="#" id="button" class="switchbutton selectDisable" style="margin-top: 30px">Switch to preview</a>
<div id="editor-container">
<!-- Editor panel -->
<h1 class="selectDisable">Markdown Editor</h1>
<textarea id="markdown-editor" class="form-control" placeholder="Write stuff here ..." aria-label="Markdown input"></textarea>
</div>
<div id="preview-container" hidden>
<h1>Preview</h1>
<iframe id="html-preview" src="/blog?sample=1"></iframe>
</div>
<div id="rawtext-container" hidden>
<h1>Raw HTML:</h1>
<pre><code id="rawtext"></code></pre>
</div>
</main>
</div>
</div>
<div id="v-break"></div>
<footer class="selectDisable"><aside><div class="selectDisable">Made by Oddbyte.</div><br /><%- include('partials/funfact') %></aside></footer>
</div>
<script>
function parsemd(inputMarkdown) {
if (!inputMarkdown) return "";
let html = inputMarkdown;
// Headings (h2, h3, h4)
html = html.replace(/^# (.*$)/gm, '<h2>$1</h2>');
html = html.replace(/^## (.*$)/gm, '<h3>$1</h3>');
html = html.replace(/^### (.*$)/gm, '<h4>$1</h4>');
// Bold text
html = html.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
// Italic text
html = html.replace(/\*(.*?)\*/g, '<i>$1</i>');
// Links
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>');
// Code blocks with language detection for syntax highlighting
// TODO: actually implement the lang stuffs in the blog page's CSS
html = html.replace(/```([a-z]*)\n([\s\S]*?)```/g, (match, lang, code) => {
if (lang) {
return `<pre><code class="language-${lang}">${code}</code></pre>`;
}
return `<pre><code>${code}</code></pre>`;
});
// Fallback for code blocks without language specification
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// Inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// Unordered list
html = html.replace(/^- (.*)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\/li>\n)+/g, '<ul>$&</ul>');
// Ordered list
html = html.replace(/^\d+\. (.*)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\/li>\n)+/g, function (match) {
// convert to <ol> if its not in <ul>
if (!match.startsWith('<ul>')) {
return '<ol>' + match + '</ol>';
}
return match;
});
// Handle newlines
html = html.replaceAll("\n", '<br />\n');
return html;
}
</script>
<script>
let editor = document.getElementById("markdown-editor");
let iframe = document.getElementById("html-preview");
let iframewindow = iframe.contentWindow;
let switchbutton = document.getElementsByClassName("switchbutton")[0];
let editorcontainer = document.getElementById("editor-container");
let previewcontainer = document.getElementById("preview-container");
let rawtextcontainer = document.getElementById("rawtext-container");
let rawtextbox = document.getElementById("rawtext");
let loaded = false;
let locked = false;
function getSelectionText() {
let text = "";
if (window.getSelection) {
text = window.getSelection().toString();
} else if (document.selection) {
text = document.selection.createRange().text;
}
return text;
}
window.addEventListener("message", (event) => {
if (event.origin !== "https://oddbyte.dev" || event.data != "Loaded" || loaded) return;
else {
// Send off current value
iframewindow.postMessage(parsemd(editor.value));
// Send off future values
editor.addEventListener("input", (event => {
iframewindow.postMessage(parsemd(editor.value));
rawtextbox.innerText = parsemd(editor.value);
}));
loaded = true;
}
});
switchbutton.addEventListener("click", (event) => {
editorcontainer.hidden = !editorcontainer.hidden;
previewcontainer.hidden = !previewcontainer.hidden;
rawtextcontainer.hidden = !rawtextcontainer.hidden;
if (!previewcontainer.hidden) switchbutton.textContent = "Switch to editor"; else switchbutton.textContent = "Switch to preview"
});
const keyDownHandler = (event) => {
if (!locked) { try { navigator.keyboard.lock(); } catch (_) {} locked = true; }
if (event.ctrlKey && (event.key === "k" || event.key === "b" || event.key === "i")) {
if (editorcontainer.hidden) return;
event.preventDefault();
event.stopPropagation();
if (event.key === "k") linkText(editor);
if (event.key === "b") boldText(editor);
if (event.key === "i") italicText(editor);
}
}
window.addEventListener("keydown", keyDownHandler);
</script>
<script>
function getInputSelection(el) {
var start = 0, end = 0, normalizedValue, range,
textInputRange, len, endRange;
if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
start = el.selectionStart;
end = el.selectionEnd;
} else {
range = document.selection.createRange();
if (range && range.parentElement() == el) {
len = el.value.length;
normalizedValue = el.value.replace(/\r\n/g, "\n");
// Create a working TextRange that lives only in the input
textInputRange = el.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
// Check if the start and end of the selection are at the very end
// of the input, since moveStart/moveEnd doesn't return what i want
// in those cases
endRange = el.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split("\n").length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
}
}
}
return {
start: start,
end: end
};
}
function replaceSelectedText(el, text) {
var sel = getInputSelection(el), val = el.value;
el.value = val.slice(0, sel.start) + text + val.slice(sel.end);
}
function linkText(el) {
var sel = getInputSelection(el), val = el.value;
// Check if the selected text starts with http:// or https://
var selectedText = val.slice(sel.start, sel.end);
var isUrl = selectedText.match(/^https?:\/\//);
if (isUrl) {
// If selected text is already a url, use it as the link target
// and use the provided text as the link text
el.value = val.slice(0, sel.start) + '[](' + selectedText + ')' + val.slice(sel.end);
} else {
// If not a url, use the provided text as the link text
// and the selected text as the link target
el.value = val.slice(0, sel.start) + '[' + selectedText + ']()' + val.slice(sel.end);
}
}
function boldText(el) {
var sel = getInputSelection(el), val = el.value;
var selectedText = val.slice(sel.start, sel.end);
el.value = val.slice(0, sel.start) + '**' + selectedText + '**' + val.slice(sel.end);
}
function italicText(el) {
var sel = getInputSelection(el), val = el.value;
var selectedText = val.slice(sel.start, sel.end);
el.value = val.slice(0, sel.start) + '*' + selectedText + '*' + val.slice(sel.end);
}
</script>
</body>
</html>