1
\$\begingroup\$

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.

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.