This commit is contained in:
Talon Poole 2021-02-15 22:31:08 +00:00
parent b1d2f7140b
commit e5bb1fa77c
5 changed files with 73 additions and 84 deletions

57
cli.js
View file

@ -2,10 +2,9 @@
import { readFileSync, existsSync } from "fs"; import { readFileSync, existsSync } from "fs";
import path from "path"; import path from "path";
import fs from "vinyl-fs"; import fs from "vinyl-fs";
import map from "map-stream";
import yargs from "yargs"; import yargs from "yargs";
import * as CSS from "./css.js"; import * as CSS from "./css.js";
import toHTML, { html } from "./html.js"; import { streamHTML, toHTML } from "./html.js";
const cli = yargs(process.argv.slice(2)) const cli = yargs(process.argv.slice(2))
.config("config", function (path) { .config("config", function (path) {
@ -47,12 +46,6 @@ const cli = yargs(process.argv.slice(2))
default: "full", default: "full",
requiresArg: true, requiresArg: true,
}, },
charset: {
type: "string",
hidden: true,
default: "utf-8",
requiresArg: true,
},
image: { image: {
type: "array", type: "array",
requiresArg: true, requiresArg: true,
@ -65,37 +58,37 @@ const cli = yargs(process.argv.slice(2))
type: "array", type: "array",
requiresArg: true, requiresArg: true,
}, },
charset: {
type: "string",
hidden: true,
default: "utf-8",
requiresArg: true,
},
modes: {
type: "boolean",
hidden: true,
default: false,
},
}); });
const CSS_VARS = CSS.rootVariables(CSS.FULL); const CSS_VARS = CSS.rootVariables(CSS.FULL);
Object.keys(CSS_VARS).map((key) => { Object.keys(CSS_VARS).map((key) => {
cli.option(key, { default: CSS_VARS[key] }); cli.option(key, { default: CSS_VARS[key] });
cli.option(key, "core");
cli.option(key, "none");
return key; return key;
}); });
cli.group(Object.keys(CSS_VARS), "CSS:");
const argv = cli const argv = cli
.conflicts("author", "body") .conflicts("author", "body")
.conflicts("descriptions", "body") .conflicts("descriptions", "body")
.conflicts("html", "body") .conflicts("html", "body")
.group(["html", "body"], "Core:") .group(["html", "body"], "Core:")
.group(["author", "descriptions", "css", "dir"], "HTML:") .group(["author", "descriptions", "css", "mode", "dir"], "HTML:")
.group(["image", "audio", "video"], "Inline Media:") .group(["image", "audio", "video"], "Inline Media:")
.group(Object.keys(CSS_VARS), "CSS:")
.alias("html", "language") .alias("html", "language")
.alias("html", "lang") .alias("html", "lang")
.coerce("css", (arg) => {
if (arg === "full") {
return CSS.stringify(CSS.FULL, { compress: true });
} else if (arg === "core") {
return CSS.stringify(CSS.CORE, { compress: true });
} else if (arg !== "none" && arg !== "") {
if (existsSync(path.resolve(arg))) {
return CSS.resolve(arg, { compress: true });
} else {
return CSS.parse(arg);
}
}
})
.showHelpOnFail(true) .showHelpOnFail(true)
.help().argv; .help().argv;
@ -105,13 +98,6 @@ if (!argv.html && !argv.body) {
cli.exit(1); cli.exit(1);
} }
let styles = Object.keys(CSS_VARS).reduce((style, key) => {
if (argv[key]) {
style += `--${key}: ${argv[key]};`;
}
return style;
}, "");
if (!argv.files) { if (!argv.files) {
let gemtext; let gemtext;
try { try {
@ -121,16 +107,9 @@ if (!argv.files) {
console.error("\nMissing files: pipe from stdin or provide [files..]"); console.error("\nMissing files: pipe from stdin or provide [files..]");
cli.exit(1); cli.exit(1);
} }
console.log(html({ contents: gemtext }, argv)); console.log(toHTML(gemtext, argv));
} else { } else {
fs.src(argv.files) fs.src(argv.files)
.pipe( .pipe(streamHTML(argv))
map(
toHTML({
...argv,
styles,
})
)
)
.pipe(fs.dest((file) => path.dirname(file.path))); .pipe(fs.dest((file) => path.dirname(file.path)));
} }

View file

@ -26,15 +26,9 @@ mobile-friendly fashion!
.P .P
\fB--body\fR \fB--body\fR
.RS 4 .RS 4
Generate only the HTML for the lines of the Gemini document. Generate just the HTML for the lines of the Gemini document.
.P .P
.RE .RE
.nf
.RS 4
gmi-web --body < doc\&.gmi
.fi
.RE
.P
\fB--html\fR \fILANG\fR \fB--html\fR \fILANG\fR
.RS 4 .RS 4
Generate a full HTML5 document with the provided \fILANG\fR. \fB--dir\fR can be used Generate a full HTML5 document with the provided \fILANG\fR. \fB--dir\fR can be used
@ -48,10 +42,10 @@ be used.
Use \fB--author\fR \fINAME\fR to set the author <meta> tag on every file. Use \fB--author\fR \fINAME\fR to set the author <meta> tag on every file.
.P .P
.RE .RE
\fB--css\fR [\fIMODE\fR|\fIFILE\fR|\fICSS\fR] \fB--css\fR [\fIMODE\fR|\fIFILE\fR]
.RS 4 .RS 4
By default this will be set to \fBfull\fR enabling a handful of customizable By default this will be set to \fBfull\fR enabling a handful of customizable
variables to be set. See --help for the complete list. variables. See \fB--help\fR for the complete list.
.P .P
.RE .RE
.nf .nf
@ -64,12 +58,11 @@ gmi-web --html en \\
.P .P
.RS 4 .RS 4
Choosing \fBcore\fR will use just what is needed to fix vertical layout issues Choosing \fBcore\fR will use just what is needed to fix vertical layout issues
with CSS 2.1's Normal Flow and inline elements. with CSS 2.1's Normal Flow and inline elements. Pointing to a .css \fIFILE\fR
will use those styles.
.P .P
Pointing to a .css \fIFILE\fR or providing a valid \fICSS\fR string will use those Choosing \fBnone\fR will not include any style information including when paired
styles. with \fB--body\fR where it will NOT apply the core inline styles.
.P
Choosing \fBnone\fR will not include any style information.
.P .P
.RE .RE
\fB[--image|--audio|--video]\fR \fIEXTENSIONS\fR \fB[--image|--audio|--video]\fR \fIEXTENSIONS\fR

View file

@ -18,8 +18,7 @@ mobile-friendly fashion!
# OPTIONS # OPTIONS
*--body* *--body*
Generate only the HTML for the lines of the Gemini document. Use *--css* none Generate just the HTML for the lines of the Gemini document.
to turn off the core inline styles.
*--html* _LANG_ *--html* _LANG_
Generate a full HTML5 document with the provided _LANG_. *--dir* can be used Generate a full HTML5 document with the provided _LANG_. *--dir* can be used
@ -32,9 +31,9 @@ mobile-friendly fashion!
Use *--author* _NAME_ to set the author <meta> tag on every file. Use *--author* _NAME_ to set the author <meta> tag on every file.
*--css* [_MODE_|_FILE_|_CSS_] *--css* [_MODE_|_FILE_]
By default this will be set to *full* enabling a handful of customizable By default this will be set to *full* enabling a handful of customizable
variables to be set. See --help for the complete list. variables. See *--help* for the complete list.
``` ```
gmi-web --html en \\ gmi-web --html en \\
@ -43,12 +42,11 @@ gmi-web --html en \\
``` ```
Choosing *core* will use just what is needed to fix vertical layout issues Choosing *core* will use just what is needed to fix vertical layout issues
with CSS 2.1's Normal Flow and inline elements. with CSS 2.1's Normal Flow and inline elements. Pointing to a .css _FILE_
will use those styles.
Pointing to a .css _FILE_ or providing a valid _CSS_ string will use those Choosing *none* will not include any style information including when paired
styles. with *--body* where it will NOT apply the core inline styles.
Choosing *none* will not include any style information.
*[--image|--audio|--video]* _EXTENSIONS_ *[--image|--audio|--video]* _EXTENSIONS_
Include media extensions inline. You can provide multiple extensions per flag Include media extensions inline. You can provide multiple extensions per flag

60
html.js
View file

@ -1,14 +1,15 @@
import fs from "fs";
import map from "map-stream";
import escape from "escape-html"; 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 GMI_REGEX = /^((=>\s?(?<href>[^\s]+)(\s(?<title>.+))?)|(?<pre>```\s?(?<alt>.+)?)|(###\s?(?<h3>.+))|(##\s?(?<h2>.+))|(#\s?(?<h1>.+))|(\*\s?(?<li>.+))|(>\s?(?<quote>.+))|(?<text>(.+)?))$/;
const truncate = (text, limit) => const truncate = (text, limit) =>
text.length > limit ? `${text.substring(0, limit)}...` : text; text.length > limit ? `${text.substring(0, limit)}...` : text;
export function html(file, options) {
const tokens = file.contents export function toHTML(gemtext, options) {
.toString("utf8") const tokens = gemtext.split("\n").map((line) => GMI_REGEX.exec(line).groups);
.split("\n")
.map((line) => GMI_REGEX.exec(line).groups);
let description = options.descriptions let description = options.descriptions
? tokens.find((token) => { ? tokens.find((token) => {
@ -36,11 +37,12 @@ ${body(tokens, options)}
export function head(options) { export function head(options) {
return ` return `
<meta charset="${options.charset}"> <meta charset="${options.charset}">
<meta name="viewport" content="width=device-width,initial-scale=1">${ <meta name="viewport" content="width=device-width,initial-scale=1">
options.css && options.css !== "" ${
? `<meta name="color-scheme" content="dark light">` options.modes || options.css === "full"
: "" ? `<meta name="color-scheme" content="dark light">\n`
}${options.css && options.css !== "" ? `<style>${options.css}</style>` : ""} : ""
}${style(options.css)}
<title>${options.title}</title>${ <title>${options.title}</title>${
!options.author ? "" : `<meta name="author" content="${options.author}">` !options.author ? "" : `<meta name="author" content="${options.author}">`
}${ }${
@ -55,6 +57,19 @@ export function head(options) {
`; `;
} }
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( function line(
{ text, href, title, pre, alt, h1, h2, h3, li, quote }, { text, href, title, pre, alt, h1, h2, h3, li, quote },
{ image, audio, video, css, body } = {} { image, audio, video, css, body } = {}
@ -63,7 +78,8 @@ function line(
if (href) { if (href) {
const titleProp = title ? ` title="${title}"` : ""; const titleProp = title ? ` title="${title}"` : "";
const FIX_NORMAL_FLOW = body && css ? ` style="display: block; max-width: 100%;"` : "" const FIX_NORMAL_FLOW =
body && css !== "none" ? ` style="display: block; max-width: 100%;"` : "";
const matchesExt = (url, exts) => const matchesExt = (url, exts) =>
exts.some((ext) => new RegExp(`\.${ext}$`).test(url)); exts.some((ext) => new RegExp(`\.${ext}$`).test(url));
@ -77,7 +93,9 @@ function line(
return `<video controls src="${href}"${titleProp}${FIX_NORMAL_FLOW}/></video>`; return `<video controls src="${href}"${titleProp}${FIX_NORMAL_FLOW}/></video>`;
} }
return `<a href="${href}"${FIX_NORMAL_FLOW}>${title ? escape(title) : href}</a>`; return `<a href="${href}"${FIX_NORMAL_FLOW}>${
title ? escape(title) : href
}</a>`;
} }
if (h1) return `<h1>${escape(h1)}</h1>`; if (h1) return `<h1>${escape(h1)}</h1>`;
@ -120,10 +138,14 @@ export function body(tokens, options) {
export const GMI_EXT = /\.gmi$/; export const GMI_EXT = /\.gmi$/;
export default (options) => (file, cb) => { export function streamHTML(options) {
if (!GMI_EXT.test(file.path)) return cb(null); return map((file, cb) => {
file.contents = Buffer.from(html(file, options)); if (!GMI_EXT.test(file.path)) return cb(null);
file.path = file.path.replace(GMI_EXT, ".html"); file.contents = Buffer.from(
if (options.verbose) console.log(file.path); toHTML(file.contents.toString("utf-8"), options)
return cb(null, file); );
}; file.path = file.path.replace(GMI_EXT, ".html");
if (options.verbose) console.log(file.path);
return cb(null, file);
});
}

View file

@ -1,3 +0,0 @@
p {
color: blue;
}