Gemtext is a lightweight markup language designed to serve content over the Gemini protocol.
You can learn more about the language here: A quick introduction to "gemtext" markup, or the complete (but still pretty short) specifications in section 5 of this document: Project Gemini Speculative specification.
Although it is perhaps contrary to the philosophy of project Gemini, I wrote a JavaScript parser to convert gemtext into HTML, with the end goal of creating a browser extension to browse the "geminispace" with a standard web browser. Current options are limited to a couple of proxies serving HTML (and processing gemtext server-side) or using dedicated software.
This is my first experience working with JavaScript and I would greatly appreciate feedback, especially as far as conventions go, but also about the logic itself. As far as I can tell, it works well and is standard-compliant, but the code feels very repetitive.
/**
* Convert a gemtext string to an HTML div element
* @param {string} gemtext string (document) to be converted
* @returns {object} converted HTML DOM element
*/
function gemtext2Html(gemtext) {
const markers = {
"h1": "#",
"h2": "##",
"h3": "###",
"link": "=>",
"quote": ">",
"list": "*",
"pre": "```"
}
var content = document.createElement("div");
var isPre = false;
var isList = false;
var currentElement;
gemtext.split(/\r?\n/).forEach( (line) => {
if (line.startsWith(markers.pre)) {
// close any open list before toggling preformated mode
if (isList) {
isList = false;
content.appendChild(currentElement)
}
isPre = !isPre;
if (isPre) {
currentElement = document.createElement("pre");
// include alt text after opening preformatting characters as class
// and replace whitespace with a CSS-compatible character
currentElement.setAttribute("class", line.substring(markers.pre.length).trim().replaceAll(/\s+/g, '-'));
}
else {
content.appendChild(currentElement);
}
return;
}
if (isPre) {
currentElement.innerHTML += line + '\r\n';
return;
}
if (line.startsWith(markers.list)) {
if (!isList) {
isList = true;
currentElement = document.createElement("ul");
}
var elem = document.createElement("li");
elem.appendChild(document.createTextNode(line.substring(markers.list.length)));
currentElement.appendChild(elem);
return;
}
else {
if (isList) {
isList = false;
content.appendChild(currentElement);
}
}
if (line.startsWith(markers.h3)) {
var elem = document.createElement("h3");
elem.appendChild(document.createTextNode(line.substring(markers.h3.length)));
content.appendChild(elem);
}
else if (line.startsWith(markers.h2)) {
var elem = document.createElement("h2");
elem.appendChild(document.createTextNode(line.substring(markers.h2.length)));
content.appendChild(elem);
}
else if (line.startsWith(markers.h1)) {
var elem = document.createElement("h1");
elem.appendChild(document.createTextNode(line.substring(markers.h1.length)));
content.appendChild(elem);
}
else if (line.startsWith(markers.quote)) {
var elem = document.createElement("blockquote");
elem.appendChild(document.createTextNode(line.substring(markers.quote.length)));
content.appendChild(elem);
}
else if (line.startsWith(markers.link)) {
line = line.substring(markers.link.length).trimStart();
ref = line.substring(0, line.search(/\s+/));
label = line.substring(ref.length);
if (label.match(/^\s*$/)){
// if label is empty, use link instead
label = ref;
}
var elem = document.createElement("a");
elem.setAttribute("href", ref);
elem.appendChild(document.createTextNode(label));
// wrap the a element in a div to display links on separate lines
var wrapper = document.createElement("div");
wrapper.setAttribute("class", "link-wrapper");
wrapper.appendChild(elem);
content.appendChild(wrapper);
}
else {
var elem = document.createElement("p");
elem.appendChild(document.createTextNode(line.trim()));
content.appendChild(elem);
}
});
// close any open pre element before returning to prevent
// content loss if gemtext doesn't include a closing marker
if (isPre) {
content.appendChild(currentElement);
}
return content;
}
You can try it out with this quick-and-dirty HTML, provided you include the code above in a gemtext2Html.js
file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Gemtext Parser Demo</title>
<style>html{max-width:70ch;margin:auto;}</style>
</head>
<body>
<label for="text-input">Enter gemtext here:</label>
<textarea id="text-input" name="text-input" rows="20" cols="80"></textarea>
<div id="output">
</div>
</body>
<script src="gemtext2Html.js"></script>
<script>
const input = document.getElementById('text-input');
const output = document.getElementById('output');
input.addEventListener('change', (e) => {
output.innerHTML = gemtext2Html(e.target.value).innerHTML;
});
</script>
</html>
Thank you for your feedback.