176 lines
4.7 KiB
JavaScript
176 lines
4.7 KiB
JavaScript
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?(?<href>[^\s]+)\s?(?<title>.+)?)|(?<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 block(
|
|
{ text, href, title, pre, alt, h1, h2, h3, li, quote },
|
|
options = {}
|
|
) {
|
|
let type = "p";
|
|
let props = "";
|
|
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) {
|
|
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 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(
|
|
Object.assign(options, {
|
|
title: tokens[0].h1,
|
|
description: description(tokens, options),
|
|
})
|
|
)}</head>
|
|
<body${options.inlineCSS("body")}>
|
|
${body(tokens, options)}
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
export function head(options) {
|
|
return `
|
|
<meta charset="${options.charset || "utf-8"}">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">${
|
|
options.styleTag
|
|
}
|
|
<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 =
|
|
options.description > 0
|
|
? tokens.find((token) => {
|
|
return token.text && token.text !== "";
|
|
})
|
|
: false;
|
|
|
|
return description && truncate(description.text, options.description);
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|