gmi-web/html.js

172 lines
4.7 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
export const GMI_REGEX = /^((=>\s?(?<href>[^\s]+)\s?(?<title>.+)?)|(?<pre>```\s?(?<alt>.+)?)|(###\s?(?<h3>.+))|(##\s?(?<h2>.+))|(#\s?(?<h1>.+))|(\*\s?(?<li>.+))|(>\s?(?<quote>.+))|(?<text>(.+)?))$/;
2021-02-19 19:32:28 +00:00
export const tokenize = (gemtext) =>
2021-02-24 19:50:41 +00:00
JSON.parse(JSON.stringify(gemtext.split("\n").map((line) => GMI_REGEX.exec(line).groups)));
2021-01-29 00:24:22 +00:00
2021-02-18 19:11:03 +00:00
export function block(
{ text, href, title, pre, alt, h1, h2, h3, li, quote },
2021-02-18 19:11:03 +00:00
options = {}
) {
let type = "p";
let props = "";
2021-02-18 23:16:05 +00:00
let content = "";
if (text) {
content = text;
}
if (h1) {
type = "h1";
content = h1;
}
if (h2) {
type = "h2";
content = h2;
}
if (h3) {
type = "h3";
content = h3;
}
if (li) {
type = "li";
content = li;
}
if (quote) {
type = "blockquote";
content = quote;
}
if (href) {
2021-02-15 21:40:59 +00:00
const matchesExt = (url, exts) =>
exts.some((ext) => new RegExp(`\.${ext}$`).test(url));
2021-02-18 19:11:03 +00:00
if (options.image && matchesExt(href, options.image)) {
type = "img";
2021-02-24 19:57:59 +00:00
props += ` src="${href}"`
props += title ? ` title="${title}"` : "";
2021-02-18 19:11:03 +00:00
} else if (options.audio && matchesExt(href, options.audio)) {
type = "audio";
2021-02-24 19:57:59 +00:00
props += ` controls src="${href}"`
props += title ? ` title="${title}"` : "";
2021-02-18 19:11:03 +00:00
} else if (options.video && matchesExt(href, options.video)) {
type = "video";
2021-02-24 19:57:59 +00:00
props += ` controls src="${href}"`
props += title ? ` title="${title}"` : "";
} else {
type = "a";
content = title || href;
2021-02-20 19:28:43 +00:00
props += ` href="${href}"`;
}
}
2021-02-18 23:16:05 +00:00
if (options.body || options.inline)
2021-02-20 20:16:36 +00:00
content !== "" ? (props += options.inlineCSS(type)) : "";
2021-02-18 23:16:05 +00:00
return `<${type}${props}>${escape(content)}</${type}>`;
}
2021-02-11 02:24:00 +00:00
export function body(tokens, options) {
let blocks = [];
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) {
blocks.push(
`<pre${cursor.alt ? ` title="${cursor.alt}"` : ""}${
2021-02-20 20:16:36 +00:00
options.body || options.inline ? options.inlineCSS("pre") : ""
}>`
);
2021-01-28 23:13:02 +00:00
const closing = tokens.findIndex((token) => token.pre);
blocks = blocks.concat(tokens.slice(0, closing).map(({ text }) => text));
blocks.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) {
blocks.push(
2021-02-20 20:16:36 +00:00
`<ul${options.body || options.inline ? options.inlineCSS("ul") : ""}>`
);
2021-01-28 23:13:02 +00:00
const closing = tokens.findIndex((token) => !token.li);
blocks = blocks
.concat([block(cursor)])
.concat(tokens.slice(0, closing).map(block));
blocks.push("</ul>");
2021-02-02 22:02:09 +00:00
tokens = tokens.slice(closing);
} else {
blocks.push(block(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
}
return blocks.join("\n");
2021-01-28 23:07:57 +00:00
}
2021-02-18 19:11:03 +00:00
export function toHTML(gemtext, options) {
2021-02-24 19:37:45 +00:00
options.inlineCSS = options.inlineCSS || CSS.inline(options);
options.styleTag = options.styleTag || CSS.style(options);
2021-02-19 19:32:28 +00:00
const tokens = tokenize(gemtext);
2021-02-18 19:11:03 +00:00
if (options.body) return body(tokens, options);
return `<!DOCTYPE html>
2021-02-24 19:37:45 +00:00
<html lang="${options.html}" dir="${
2021-02-24 20:23:28 +00:00
options.dir || "ltr"
2021-02-20 20:16:36 +00:00
}" style='${options.inlineCSS("html")}'>
2021-02-18 19:11:03 +00:00
<head>${head(
Object.assign(options, {
title: tokens[0].h1,
description: description(tokens, options),
})
)}</head>
2021-02-20 20:16:36 +00:00
<body${options.inlineCSS("body")}>
2021-02-18 19:11:03 +00:00
${body(tokens, options)}
</body>
</html>
`;
}
export function head(options) {
return `
2021-02-24 20:23:28 +00:00
<meta charset="${options.charset || "utf-8"}">
2021-02-20 21:22:37 +00:00
<meta name="viewport" content="width=device-width,initial-scale=1">${
options.styleTag
}
2021-02-18 19:11:03 +00:00
<title>${options.title}</title>${
!options.author ? "" : `\n<meta name="author" content="${options.author}">`
}${
!options.description
? ""
: `\n<meta name="description" content="${escape(options.description)}">`
}${
!options.canonical
? ""
: `\n<link rel="canonical" href="${options.canonical}">`
}
`;
}
function description(tokens, options) {
const truncate = (text, limit) =>
text.length > limit ? `${text.substring(0, limit)}...` : text;
let description =
2021-02-24 20:23:28 +00:00
options.description > 0
2021-02-18 19:11:03 +00:00
? tokens.find((token) => {
return token.text && token.text !== "";
})
: false;
2021-02-24 20:23:28 +00:00
return description && truncate(description.text, options.description);
2021-02-18 19:11:03 +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) {
2021-02-24 19:37:45 +00:00
options.inlineCSS = options.inlineCSS || CSS.inline(options);
options.styleTag = options.styleTag || CSS.style(options);
2021-02-15 22:31:08 +00:00
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);
});
}