import fs from "fs"; import map from "map-stream"; import escape from "escape-html"; import * as CSS from "./css.js"; export const GMI_REGEX = /^((=>\s?(?[^\s]+)\s?(?.+)?)|(?<pre>```\s?(?<alt>.+)?)|(###\s?(?<h3>.+))|(##\s?(?<h2>.+))|(#\s?(?<h1>.+))|(\*\s?(?<li>.+))|(>\s?(?<quote>.+))|(?<text>(.+)?))$/; export const tokenize = (gemtext) => JSON.parse( JSON.stringify( gemtext.split("\n").map((line) => GMI_REGEX.exec(line).groups) ) ); export function toHTML(gemtext, options) { options.inlineCSS = options.inlineCSS || CSS.inline(options); options.styleTag = options.styleTag || CSS.style(options); const tokens = tokenize(gemtext); if (options.body) return body(tokens, options); return `<!DOCTYPE html> <html lang="${options.html}" dir="${ options.dir || "ltr" }" style='${options.inlineCSS("html")}'> <head>${head(tokens, options)}</head> <body${options.inlineCSS("body")}> ${body(tokens, options)} </body> </html> `; } export function block( { text, href, title, pre, alt, h1, h2, h3, li, quote }, options = {} ) { let type = "p"; let props = ""; let content = ""; if (text) { content = text; } if (li) { type = "li"; content = li; } if (quote) { type = "blockquote"; content = quote; } if (h1) { type = "h1"; content = h1; } if (h2) { type = "h2"; content = h2; } if (h3) { type = "h3"; content = h3; } if (href) { const matchesExt = (url, exts) => exts.some((ext) => new RegExp(`\.${ext}$`).test(url)); if (options.image && matchesExt(href, options.image)) { type = "img"; props += ` src="${href}"`; props += title ? ` title="${title}"` : ""; } else if (options.audio && matchesExt(href, options.audio)) { type = "audio"; props += ` controls src="${href}"`; props += title ? ` title="${title}"` : ""; } else if (options.video && matchesExt(href, options.video)) { type = "video"; props += ` controls src="${href}"`; props += title ? ` title="${title}"` : ""; } else { type = "a"; content = title || href; props += ` href="${href}"`; } } if (options.body || options.inline) { props += options.inlineCSS( type === "p" && content === "" ? "p:empty" : type ); } return `<${type}${props}>${escape(content)}</${type}>`; } export function body(tokens, options) { let blocks = []; let cursor = tokens.shift(); while (tokens.length) { if (cursor.pre) { blocks.push( `<pre${cursor.alt ? ` title="${cursor.alt}"` : ""}${ options.body || options.inline ? options.inlineCSS("pre") : "" }>` ); const closing = tokens.findIndex((token) => token.pre); blocks = blocks.concat(tokens.slice(0, closing).map(({ text }) => text)); blocks.push("</pre>"); tokens = tokens.slice(closing + 1); } else if (cursor.li) { blocks.push( `<ul${options.body || options.inline ? options.inlineCSS("ul") : ""}>` ); const closing = tokens.findIndex((token) => !token.li); blocks = blocks .concat([block(cursor)]) .concat(tokens.slice(0, closing).map(block)); blocks.push("</ul>"); tokens = tokens.slice(closing); } else { blocks.push(block(cursor, options)); } cursor = tokens.shift(); } return blocks.join("\n"); } export function head(tokens, options) { const truncate = (text, limit) => text.length > limit ? `${text.substring(0, limit)}...` : text; let description = options.description > 0 ? tokens.find((token) => { return token.text && token.text !== ""; }) : false; return ` <meta charset="${options.charset || "utf-8"}"> <meta name="viewport" content="width=device-width,initial-scale=1">${ options.styleTag } <title>${tokens.find(({ h1 }) => h1).h1 || ""}${ options.author ? `\n` : "" }${ description ? `\n` : "" }${ options.canonical ? `\n` : "" } `; } export const GMI_EXT = /\.gmi$/; export function streamHTML(options) { options.inlineCSS = options.inlineCSS || CSS.inline(options); options.styleTag = options.styleTag || CSS.style(options); return map((file, cb) => { if (!GMI_EXT.test(file.path)) return cb(null); file.contents = Buffer.from( toHTML(file.contents.toString("utf-8"), options) ); file.path = file.path.replace(GMI_EXT, ".html"); return cb(null, file); }); }