// GemPress gmi-to-html utility, using Sugar-C in less than 190 lines of code // 2021, by Antonio Prates #include #define DEFAULT_TITLE "#\n" // globals (hold some state during parsing of each file) string title; bool preformattedMode; bool itemsMode; number linkCount; string htmlTag = "\n"; // html escape code helpers string escapeQ(string text) { return replaceWord(text, "\"", """); } string escapeE(string text) { return replaceWord(text, "&", "&"); } string escapeLt(string text) { return replaceWord(text, "<", "<"); } string escapeGt(string text) { return replaceWord(text, ">", ">"); } // removes/escapes/repaces special chars and splits buffer to lines stringList preProcess(string text) { text = replaceWord(text, "\r", ""); // remove \r to support LF/CRLF text = escapeGt(escapeLt(escapeE(text))); // html escape codes text = replaceWord(text, "\n", "\n
\n"); // swap to break line code return splitSep(text, '\n'); // ...and split to lines } // join lines back to single buffer, fix spacing issues and wrap body to html string postProcess(number linesCount, stringList lines) { // build body contents by joining html lines back to single buffer string body = joinSep(linesCount, lines, '\n'); // hacks and fixes for spacing issues body = replaceWord(body, "\n\n", "\n"); body = replaceWord(body, "\n
\n
", ""); body = replaceWord(body, "\n
", ""); body = replaceWord(body, "\n
\n
", ""); body = replaceWord(body, "\n
", ""); body = replaceWord(body, "\n
\n
", ""); body = replaceWord(body, "\n
", ""); body = replaceWord(body, "\n
\n
", ""); body = replaceWord(body, "\n
", ""); body = replaceWord(body, "\n
", ""); body = replaceWord(body, "
\n
", "
"); body = replaceWord(body, "
\n
  • ", "
  • "); body = replaceWord(body, "
    \n
    \n", ""); body = replaceWord(body, "
    \n", ""); // wrap body into html boilerplate (add title to head) string head = join5s("\n", htmlTag, "\n\n" "\n" "\n" "\n", title, "\n\n
    \n
    \n"); string ending = "\n
    \n
    \n
    \n
    \n\n\n"; return replaceWord(join3s(head, body, ending), "\n", "\r\n"); // add back \r } // convert a line starting with => to html link string toLink(string line) { stringList words = splitSep(line, ' '); // tokenize the link number wordCount = listCount(words); // count our tokens if (wordCount < 2) // fail-safe for missing link return line; string url = words[1]; // get url bool isInternal = true; // adapt internal/external links: string externalRef[6] = {"http://", "https://", "ftp://", "gemini://", "gopher://", "www."}; for (number i = 0; i < 6; i++) if (startsWith(url, externalRef[i])) isInternal = false; url = isInternal ? replaceWord(url, ".gmi", ".html") : url; string description = // join the rest as description OR use url as desc. wordCount > 2 ? joinSep(listCount(&words[2]), &words[2], ' ') : url; string linkNumber = join3s("[", ofNumber(++linkCount), "] ", description, ""); } // parse line and do markdown substitutions for equivalent html tags string lineToHTML(string line) { // if (startsWith(line, "```")) { // ``` -> preformatted text preformattedMode = !preformattedMode; // toggle global
     mode
        if (!preformattedMode)
          return replaceWord(line, "```", "
    "); if (strlen(line) < 5) return "
    ";
        return join3s("
    ");
      }
      if (preformattedMode)                     // while global 
     mode
        return replaceWord(line, "
    ", ""); // -> revert preProcess // if (startsWith(line, "> ")) { // > -> blockquote (escaped) line = replaceWord(line, "
    ", ""); // -> revert preProcess return join3s("
    ", &line[5], "
    \n"); } // if (startsWith(line, "=>")) // => link (escaped) return toLink(line); // -> if (startsWith(line, "### ")) // ### -> h3 return join3s("

    ", &line[4], "

    "); if (startsWith(line, "## ")) // ## -> h2 return join3s("

    ", &line[3], "

    "); if (startsWith(line, "# ")) { // # -> h1 ...AND set global page title! if (areSame(title, DEFAULT_TITLE)) // (stick to first h1 found) title = join3s("", &line[2], "\n"); return join3s("

    ", &line[2], "

    "); } // if (areSame(line, "---")) return "
    "; // number length; number ticksCount = countWord(line, "`"); // futile attempt to prevent bleeding if (ticksCount > 1 && ticksCount % 2 == 0) { // simple check, no guarantees line = replaceWord(line, " `", " "); // start inline code line = replaceWord(line, "` ", " "); // end inline code if (startsWith(line, "`")) // if first thin in the line line = join2s("", &line[1]); // begin with inline code length = strlen(line); if (length > 0 && line[length - 1] == '`') // if last... line = replaceWord(line, "`", ""); // end inline code } // (accepts inline code) if (startsWith(line, "* ")) { // * -> list item itemsMode = true; // toggle global
     mode
        return join3s("
  • ", &line[2], "
  • "); // end inline bold text } // ...or else -> simple text return line; } // try to get the file from path and generate the corresponding html in place void convert(string path) { if (startsWith(path, "-lang:")) { htmlTag = join3s("\n"); // set langCode return; } if (countWord(path, ".gmi")) { // ultra-simple path validadion string text = readFile(path); // read and hope content is text/gemini :D if (text) { // safe-gard / read failure title = DEFAULT_TITLE; // reset global 'page title' for each file preformattedMode = false; // reset global '
     mode' for each file
          linkCount = 0;              // reset global 'link count' for each file
          bool wasItemsMode = false;
          stringList lines = preProcess(text);
          number linesCount = listCount(lines);
          for (number i = 0; i < linesCount; i++) {
            if (itemsMode && areSame(lines[i], "
    ")) continue; itemsMode = false; // reset global 'items mode' for each file lines[i] = lineToHTML(lines[i]); // do the magic :D if (itemsMode && !wasItemsMode) lines[i] = join2s("
      \n", lines[i]); if (wasItemsMode && !itemsMode) lines[i] = join2s("
    \n", lines[i]); wasItemsMode = itemsMode; } string newPath = replaceWord(path, ".gmi", ".html"); if (writeFile(postProcess(linesCount, lines), newPath)) // and save... return println(newPath); } println(join2s(path, " -> FAILED!")); // ...or complain! } } app({ if (argc < 2) println("please, provide '.gmi' file paths as arguments..."); else forEach(argc - 1, &argv[1], &convert); })