gmi-web/html.js
Talon Poole 2ade53f268 three --css modes: gmi.css, base and none
when "none" inline tags will be wrapped in <p>

--image, --audio and --video now accept the extensions to inline
  so what's possible to load isn't maintained here

--lang is now referred to as --html
  --html and --body are mutually exclusive
2021-02-11 23:26:29 +00:00

128 lines
3.7 KiB
JavaScript

import escape from "escape-html";
export const GMI_REGEX = /^((=>\s?(?<href>[^\s]+)(\s(?<title>.+))?)|(?<pre>```\s?(?<alt>.+)?)|(###\s?(?<h3>.+))|(##\s?(?<h2>.+))|(#\s?(?<h1>.+))|(\*\s?(?<li>.+))|(>\s?(?<quote>.+))|(?<text>(.+)?))$/;
export function html(file, options) {
const tokens = file.contents
.toString("utf8")
.split("\n")
.map((line) => GMI_REGEX.exec(line).groups);
if (options.body) return body(tokens, options);
return `<!DOCTYPE html>
<html lang="${options.language}" style="${options.styles}">
<head>${head(
Object.assign(options, {
title: tokens[0].h1,
charset: "utf-8",
})
)}</head>
<body>
${body(tokens, options)}
</body>
</html>
`;
}
export const BASE_CSS =
"p,a,pre,h1,h2,h3,ul,blockquote,img,audio,video{display:block;max-width:100%;margin:0;padding:0;overflow-wrap:anywhere;}";
export function head(options) {
return `
${
options.css !== ""
? `<meta name="color-scheme" content="dark light">`
: ""
}
${options.css !== "" ? `<style>${options.css}</style>` : ""}
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta charset="${options.charset}">
<meta language="${options.language}">
<title>${options.title}</title>${
!options.description
? ""
: `<meta name="description" content="${options.description}}">`
}${
!options.canonical
? ""
: `<link rel="canonical" href="${options.canonical}}">`
}
`;
}
function line(
{ text, href, title, pre, alt, h1, h2, h3, li, quote },
{ image, audio, video, css } = {}
) {
if (text) return `<p>${escape(text)}</p>`;
const matchesExt = (url, exts) =>
exts.some((ext) => new RegExp(`\.${ext}$`).test(url));
if (href) {
const titleProp = title ? ` title="${title}"` : "";
if (image && matchesExt(href, image)) {
return `<img src="${href}"${titleProp}/>`;
}
if (audio && matchesExt(href, audio)) {
if (css === "") {
return `<p><audio controls src="${href}"${titleProp}></audio><p>`;
}
return `<audio controls src="${href}"${titleProp}></audio>`;
}
if (video && matchesExt(href, video))
return `<video controls src="${href}"${titleProp}/></video>`;
if (css === "") {
return `<p><a href="${href}">${title ? escape(title) : href}</a></p>`;
}
return `<a href="${href}">${title ? escape(title) : href}</a>`;
}
if (h1) return `<h1>${escape(h1)}</h1>`;
if (h2) return `<h2>${escape(h2)}</h2>`;
if (h3) return `<h3>${escape(h3)}</h3>`;
if (li) return `<li>${escape(li)}</li>`;
if (quote) return `<blockquote>${escape(quote)}</blockquote>`;
return `<p><br></p>`;
}
export function body(tokens, options) {
let lines = [];
let cursor = tokens.shift();
while (tokens.length) {
if (cursor.pre) {
lines.push(`<pre${cursor.alt ? ` title="${cursor.alt}"` : ""}>`);
const closing = tokens.findIndex((token) => token.pre);
lines = lines.concat(tokens.slice(0, closing).map(({ text }) => text));
lines.push("</pre>");
tokens = tokens.slice(closing + 1);
} else if (cursor.li) {
lines.push(`<ul>`);
const closing = tokens.findIndex((token) => !token.li);
lines = lines
.concat([line(cursor)])
.concat(tokens.slice(0, closing).map(line));
lines.push("</ul>");
tokens = tokens.slice(closing);
} else {
lines.push(line(cursor, options));
}
cursor = tokens.shift();
}
return lines.join("\n");
}
export const GMI_EXT = /\.gmi$/;
export default (options) => (file, cb) => {
if (!GMI_EXT.test(file.path)) return cb(null);
file.contents = Buffer.from(html(file, options));
file.path = file.path.replace(GMI_EXT, ".html");
if (options.verbose) console.log(file.path);
return cb(null, file);
};