hotn-gay-digital-garden/script.js
2026-02-15 21:39:13 -06:00

240 lines
No EOL
8.3 KiB
JavaScript

import { marked } from 'https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js'
const body = document.body
async function loadPage(x = 0, y = 0) {
const urlString = window.location.search;
let pageName = urlString.split('?')[1];
if(!pageName || !(/^[A-Za-z0-9_]+$/.test(pageName))) {
pageName = "home_page"
window.location.search = "?home_page"
window.history.pushState("home_page", "", window.location)
}
const text = await requestPage(pageName);
const steps = await stepThroughPage(pageName);
renderPage(pageName, text, steps, x, y);
}
async function stepThroughPage(pageName) {
const step = await fetch(`/loam/step/${pageName}.txt`, { method: 'post' });
if(!step.ok) { console.log("Unable to leave footprints here :("); }
const getSteps = await fetch(`/hidden/footsteps/${pageName}.txt`);
if(!getSteps.ok) { console.log("Unable to figure out how many steps are here :("); return "an unknown number of " }
return getSteps.text();
}
async function requestPage(pageName) {
const fileName = pageName.replaceAll("__", "/") + ".md";
const response = await fetch(fileName);
if(!response.ok) {
return "This page does not exist!"
}
return response.text();
}
async function preprocessMarkdown(markdown) {
// parse the initial {{{ frontmatter }}}
let frontmatter = null;
markdown = markdown.trimStart();
if (markdown.startsWith("{{{")) {
let begin = markdown.indexOf("{{{") + 2;
let end = markdown.indexOf("}}}") + 1;
// console.log(`begin: ${begin} end: ${end}`);
// console.log(markdown.slice(begin, end));
// parse frontmatter JSON
frontmatter = JSON.parse(markdown.slice(begin, end));
// remove frontmatter
markdown = markdown.slice(end + 2);
}
// use frontmatter to add page stylings
if (frontmatter) {
const title = frontmatter.title ? `# ${frontmatter.title}\n---\n\n` : "";
const status = frontmatter.status ? `*<a class="metadata" href="?metadata">status: ${frontmatter.status}</a>*\n\n---\n\n` : "";
let metadata = (frontmatter.nobreak ? "" : "\n---") + `\n\n*<a class="metadata" href="?metadata"><p>`;
if (frontmatter.last_tended_to) {
metadata += `last tended to: ${frontmatter.last_tended_to}`;
}
if (frontmatter.repotted) {
if(!metadata.endsWith("<p>")) { // not the first metadata item
metadata += "<br>";
}
metadata += `repotted: ${frontmatter.repotted}`;
}
if (frontmatter.planted) {
if(!metadata.endsWith("<p>")) { // not the first metadata item
metadata += "<br>";
}
metadata += `planted: ${frontmatter.planted}`;
}
metadata += "</p></a>*";
if (frontmatter.allow_inline) {
const matches = markdown.match(/\[\[.+\]\]/g);
let i = 0;
for(let match of matches) {
match = match.slice(2, -2);
if(i < 2) { // synchronously load the first few inlines
let subpage = await requestPage(`${match}`);
subpage = await preprocessMarkdown(subpage);
markdown = markdown.replace(`[[${match}]]`, subpage);
}
else { // replace the rest with placeholders to be loaded slowly
markdown = markdown.replace(`[[${match}]]`,
`<p id="placeholder-${match}"></p>`
)
requestPage(match)
.then((text) => preprocessMarkdown(text))
.then((md) => {
const placeholder = document.getElementById(`placeholder-${match}`);
placeholder.outerHTML = marked.parse(md);
});
}
i++;
}
}
markdown = title + status + markdown + metadata;
}
return markdown;
}
async function renderPage(title, markdown, steps, x = 0, y = 0) {
let clientWidth = window.innerWidth;
// make sure a page isn't wider than the screen
const pageWidth = Math.min(400, clientWidth - 20);
// make sure that small screens can still get the corkboard effect
if(clientWidth < pageWidth * 2) {
clientWidth = pageWidth * 2;
}
if(x + pageWidth + 20 > clientWidth) {
// offset the page by the amount it would've been pushed off the page.
x -= ((x + pageWidth + 20) - clientWidth) * 2;
}
x = Math.max(0, Math.min(x, clientWidth - pageWidth - 20))
let page = document.createElement("div");
page.id = title;
page.className = "page";
page.style = `width: ${pageWidth}px; left: ${x}px; top: ${y}px`;
markdown = await preprocessMarkdown(markdown);
if(steps) {
markdown += `<p>*this page has been passed through ${steps}`
markdown += steps === "1" ? " time" : " times"
markdown += "*<p>"
}
page.innerHTML = marked.parse(markdown);
body.appendChild(page);
}
// satisfies the following constraints for a container:
// 1. The container cannot be *outside the screen bounds*
// a. If this means resizing the container width, do so
// 2. The container must be underneath the mouse pointer (with the container top placed aligned with it)
// returns { x = <top left X value>, y = <top left y value>, width = <modified container width> }
function placeContainer(width, mouseX, mouseY) {
// TODO ( i got lazy )
}
async function renderImageDetail(source, width, x = 0, y = 0) {
let clientWidth = window.visualViewport.width;
if (width < 400) width = 400;
// make sure a page isn't wider than the screen
const pageWidth = Math.min(width, clientWidth - 20);
// start by putting the image in the center of the screen
let newX = window.visualViewport.offsetLeft + clientWidth / 2 - width / 2;
// make sure the mouse pointer is past the *left edge* of the image
if(x < newX) newX = x;
// make sure the mouse pointer is before the *right edge* of the image
if(newX + pageWidth < x) newX = x - pageWidth;
// make sure the page is not off screen either direction
if(newX < window.visualViewport.offsetLeft) newX = 0;
if(newX + pageWidth > window.visualViewport.offsetLeft + clientWidth) newX = clientWidth - pageWidth;
let page = document.createElement("div");
page.id = source;
page.className = "page";
page.style = `width: ${pageWidth}px; left: ${newX}px; top: ${y}px`;
let img = document.createElement("img");
page.appendChild(img);
img.src = source;
img.className = "imgDetail";
body.appendChild(page);
}
window.addEventListener('popstate', (event) => {
if(event.state) {
const name = event.state;
const matches = document.querySelectorAll(`#${name}`);
const keep = matches[matches.length - 1];
if(keep) {
while(keep.nextElementSibling) {
keep.nextElementSibling.remove();
}
} else { // no pages remaining, just reload the one requested.
loadPage();
}
}
});
window.addEventListener('click', (event) => {
// console.log(event.target.tagName);
if(event.target.tagName === "A") {
const url = event.target.href;
if(!url.includes('hotn.gay') || url.includes('mailto')) { return; }
event.preventDefault();
let page = url.split("?")[1]
if(page != window.history.state) window.history.pushState(page, "", url);
loadPage(event.pageX, event.pageY);
} else if(event.target.tagName === "IMG") {
if(event.target.className === "imgDetail") { event.target.parentElement.remove(); return; }
let src = event.target.src;
if(document.getElementById(src)) { document.getElementById(src).remove(); }
let width = event.target.naturalWidth + 20;
renderImageDetail(src, width, event.pageX, event.pageY);
} else {
let keep = event.target.closest('.page');
if(keep) {
window.history.pushState(keep.id, "", `?${keep.id}`)
while(keep.nextElementSibling) {
keep.nextElementSibling.remove();
}
}
}
})
loadPage();