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 and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.