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 function toHTML(gemtext, options) { const tokens = gemtext.split("\n").map((line) => GMI_REGEX.exec(line).groups); let description = options.descriptions > 0 ? tokens.find((token) => { return token.text && token.text !== ""; }) : false; if (options.body) return body(tokens, options); const truncate = (text, limit) => text.length > limit ? `${text.substring(0, limit)}...` : text; function overrideStyles(options) { if (options.css !== "full") return ""; const vars = CSS.rootVariables(CSS.FULL); return Object.keys(vars).reduce((styles, key) => { if (options[key] && options[key] !== vars[key]) styles += `--${key}: ${options[key]};`; return styles; }, ""); } return `<!DOCTYPE html> <html lang="${options.language}" dir="${options.dir}" style="${overrideStyles( options )}"> <head>${head( Object.assign(options, { title: tokens[0].h1, description: description && truncate(description.text, options.descriptions), }) )}</head> <body> ${body(tokens, options)} </body> </html> `; } export function head(options) { return ` <meta charset="${options.charset}"> <meta name="viewport" content="width=device-width,initial-scale=1"> ${ options.modes || options.css === "full" ? `<meta name="color-scheme" content="dark light">\n` : "" }${style(options.css)} <title>${options.title}${ !options.author ? "" : `\n` }${ !options.description ? "" : `\n` }${ !options.canonical ? "" : `\n` } `; } export function style(mode) { if (mode === "none") return ""; if (mode === "core") return ``; if (mode === "full") return ``; if (fs.existsSync(mode)) { return ``; } else { throw new Error(`Cannot find file ${mode}.`); } } function line( { text, href, title, pre, alt, h1, h2, h3, li, quote }, { image, audio, video, css, body } = {} ) { if (text) return `

${escape(text)}

`; if (href) { const titleProp = title ? ` title="${title}"` : ""; const FIX_NORMAL_FLOW = body && css !== "none" ? ` style="display: block; max-width: 100%;"` : ""; const matchesExt = (url, exts) => exts.some((ext) => new RegExp(`\.${ext}$`).test(url)); if (image && matchesExt(href, image)) { return ``; } if (audio && matchesExt(href, audio)) { return ``; } if (video && matchesExt(href, video)) { return ``; } return `${ title ? escape(title) : href }`; } if (h1) return `

${escape(h1)}

`; if (h2) return `

${escape(h2)}

`; if (h3) return `

${escape(h3)}

`; if (li) return `
  • ${escape(li)}
  • `; if (quote) return `
    ${escape(quote)}
    `; return `


    `; } export function body(tokens, options) { let lines = []; let cursor = tokens.shift(); while (tokens.length) { if (cursor.pre) { lines.push(``); const closing = tokens.findIndex((token) => token.pre); lines = lines.concat(tokens.slice(0, closing).map(({ text }) => text)); lines.push(""); tokens = tokens.slice(closing + 1); } else if (cursor.li) { lines.push(`"); tokens = tokens.slice(closing); } else { lines.push(line(cursor, options)); } cursor = tokens.shift(); } return lines.join("\n"); } export const GMI_EXT = /\.gmi$/; export function streamHTML(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"); if (options.verbose) console.log(file.path); return cb(null, file); }); }