gmi-web/html.js

154 lines
4.5 KiB
JavaScript
Raw Normal View History

2021-02-15 22:31:08 +00:00
import fs from "fs";
import map from "map-stream";
import escape from "escape-html";
2021-02-15 22:31:08 +00:00
import * as CSS from "./css.js";
2021-01-30 16:32:44 +00:00
2021-02-11 02:24:00 +00:00
export const GMI_REGEX = /^((=>\s?(?<href>[^\s]+)(\s(?<title>.+))?)|(?<pre>```\s?(?<alt>.+)?)|(###\s?(?<h3>.+))|(##\s?(?<h2>.+))|(#\s?(?<h1>.+))|(\*\s?(?<li>.+))|(>\s?(?<quote>.+))|(?<text>(.+)?))$/;
2021-01-29 00:24:22 +00:00
const truncate = (text, limit) =>
text.length > limit ? `${text.substring(0, limit)}...` : text;
2021-02-15 22:31:08 +00:00
export function toHTML(gemtext, options) {
const tokens = gemtext.split("\n").map((line) => GMI_REGEX.exec(line).groups);
2021-02-11 02:24:00 +00:00
2021-02-15 22:56:53 +00:00
let description =
options.descriptions > 0
? tokens.find((token) => {
return token.text && token.text !== "";
})
: false;
2021-02-11 02:24:00 +00:00
if (options.body) return body(tokens, options);
2021-01-29 23:17:25 +00:00
return `<!DOCTYPE html>
2021-02-15 17:17:16 +00:00
<html lang="${options.language}" dir="${options.dir}" style="${options.styles}">
2021-02-11 02:24:00 +00:00
<head>${head(
Object.assign(options, {
title: tokens[0].h1,
2021-02-15 22:56:53 +00:00
description:
description && truncate(description.text, options.descriptions),
2021-02-11 02:24:00 +00:00
})
)}</head>
<body>
${body(tokens, options)}
</body>
2021-01-29 22:32:30 +00:00
</html>
2021-01-29 23:17:25 +00:00
`;
2021-02-11 02:24:00 +00:00
}
export function head(options) {
return `
<meta charset="${options.charset}">
2021-02-15 22:31:08 +00:00
<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)}
2021-02-11 02:24:00 +00:00
<title>${options.title}</title>${
2021-02-15 22:56:53 +00:00
!options.author ? "" : `\n<meta name="author" content="${options.author}">`
}${
2021-02-11 02:24:00 +00:00
!options.description
? ""
2021-02-15 22:56:53 +00:00
: `\n<meta name="description" content="${escape(options.description)}">`
2021-01-29 23:17:25 +00:00
}${
!options.canonical
? ""
2021-02-15 22:56:53 +00:00
: `\n<link rel="canonical" href="${options.canonical}">`
2021-01-28 23:13:02 +00:00
}
2021-01-29 22:32:30 +00:00
`;
}
2021-01-28 23:07:57 +00:00
2021-02-15 22:31:08 +00:00
export function style(mode) {
if (mode === "none") return "";
if (mode === "core")
return `<style>${CSS.stringify(CSS.CORE, { compress: true })}</style>`;
if (mode === "full")
return `<style>${CSS.stringify(CSS.FULL, { compress: true })}</style>`;
if (fs.existsSync(mode)) {
return `<style>${CSS.resolve(mode, { compress: true })}</style>`;
} else {
throw new Error(`Cannot find file ${mode}.`);
}
}
function line(
{ text, href, title, pre, alt, h1, h2, h3, li, quote },
2021-02-15 21:40:59 +00:00
{ image, audio, video, css, body } = {}
) {
if (text) return `<p>${escape(text)}</p>`;
if (href) {
const titleProp = title ? ` title="${title}"` : "";
2021-02-15 22:31:08 +00:00
const FIX_NORMAL_FLOW =
body && css !== "none" ? ` style="display: block; max-width: 100%;"` : "";
2021-02-15 21:40:59 +00:00
const matchesExt = (url, exts) =>
exts.some((ext) => new RegExp(`\.${ext}$`).test(url));
if (image && matchesExt(href, image)) {
2021-02-15 21:40:59 +00:00
return `<img src="${href}"${titleProp}${FIX_NORMAL_FLOW}/>`;
}
if (audio && matchesExt(href, audio)) {
2021-02-15 21:40:59 +00:00
return `<audio controls src="${href}"${titleProp}${FIX_NORMAL_FLOW}></audio>`;
}
if (video && matchesExt(href, video)) {
return `<video controls src="${href}"${titleProp}${FIX_NORMAL_FLOW}/></video>`;
}
2021-02-15 22:31:08 +00:00
return `<a href="${href}"${FIX_NORMAL_FLOW}>${
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>`;
}
2021-02-11 02:24:00 +00:00
export function body(tokens, options) {
let lines = [];
2021-01-28 23:07:57 +00:00
2021-01-28 23:13:02 +00:00
let cursor = tokens.shift();
2021-01-28 23:07:57 +00:00
while (tokens.length) {
if (cursor.pre) {
2021-02-11 02:24:00 +00:00
lines.push(`<pre${cursor.alt ? ` title="${cursor.alt}"` : ""}>`);
2021-01-28 23:13:02 +00:00
const closing = tokens.findIndex((token) => token.pre);
2021-02-11 02:24:00 +00:00
lines = lines.concat(tokens.slice(0, closing).map(({ text }) => text));
lines.push("</pre>");
2021-01-28 23:13:02 +00:00
tokens = tokens.slice(closing + 1);
2021-01-30 21:28:30 +00:00
} else if (cursor.li) {
2021-02-11 02:24:00 +00:00
lines.push(`<ul>`);
2021-01-28 23:13:02 +00:00
const closing = tokens.findIndex((token) => !token.li);
2021-02-11 02:24:00 +00:00
lines = lines
2021-02-02 22:02:09 +00:00
.concat([line(cursor)])
.concat(tokens.slice(0, closing).map(line));
2021-02-11 02:24:00 +00:00
lines.push("</ul>");
2021-02-02 22:02:09 +00:00
tokens = tokens.slice(closing);
} else {
2021-02-11 02:24:00 +00:00
lines.push(line(cursor, options));
2021-01-28 23:07:57 +00:00
}
2021-01-28 23:13:02 +00:00
cursor = tokens.shift();
2021-01-28 23:07:57 +00:00
}
2021-02-11 02:24:00 +00:00
return lines.join("\n");
2021-01-28 23:07:57 +00:00
}
2021-02-11 02:24:00 +00:00
export const GMI_EXT = /\.gmi$/;
2021-02-15 22:31:08 +00:00
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);
});
}