update --css

This commit is contained in:
Talon Poole 2021-02-15 20:56:41 +00:00
parent 74e84b1ab4
commit 77e4ecb2fb
8 changed files with 158 additions and 118 deletions

122
cli.js
View file

@ -1,24 +1,24 @@
#!/usr/bin/env node #!/usr/bin/env node
import { readFileSync } 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 map from "map-stream";
import yargs from "yargs"; import yargs from "yargs";
import CleanCSS from "clean-css"; import * as CSS from "./css.js";
import toHTML, { BASE_CSS, html } from "./html.js"; import toHTML, { html } 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) {
return JSON.parse(readFileSync(path)); return JSON.parse(readFileSync(path), "utf-8");
}) })
.scriptName("gmi-web") .scriptName("gmi-web")
.command("$0 [files..]", "Convert text/gemini to text/html.", (yargs) => .command("$0 [files..]", "Convert text/gemini to text/html.", (yargs) =>
yargs yargs
.example("$0 --body < ~/my-capsule/index.gmi")
.example("$0 --html en $(find ~/my-capsule -name '*.gmi')") .example("$0 --html en $(find ~/my-capsule -name '*.gmi')")
.example( .example(
"$0 --foreground '#000000' --background '#EEEEEE' --html en < doc.gmi" "$0 --foreground '#000000' --background '#EEEEEE' --html en < doc.gmi"
) )
.example("$0 --body < ~/my-capsule/index.gmi")
.example("$0 --image jpg --audio mp3 --image png --body < doc.gmi") .example("$0 --image jpg --audio mp3 --image png --body < doc.gmi")
.epilog("See the gmi-web(1) man page for more information") .epilog("See the gmi-web(1) man page for more information")
) )
@ -44,8 +44,7 @@ const cli = yargs(process.argv.slice(2))
type: "boolean", type: "boolean",
}, },
css: { css: {
choices: ["gmi.css", "base", "none"], default: "full",
default: "gmi.css",
requiresArg: true, requiresArg: true,
}, },
charset: { charset: {
@ -54,99 +53,70 @@ const cli = yargs(process.argv.slice(2))
default: "utf-8", default: "utf-8",
requiresArg: true, requiresArg: true,
}, },
image: {
type: "array",
requiresArg: true,
},
audio: {
type: "array",
requiresArg: true,
},
video: {
type: "array",
requiresArg: true,
},
}); });
cli.options({ const CSS_VARS = CSS.rootVariables(CSS.FULL);
image: { Object.keys(CSS_VARS).map((key) => {
type: "array", cli.option(key, { default: CSS_VARS[key] });
requiresArg: true,
},
audio: {
type: "array",
requiresArg: true,
},
video: {
type: "array",
requiresArg: true,
},
});
// TODO: automatically pull these in from gmi.css (also for gmi.css(5))
const GMI_CSS_VARS = [
"body-width",
"foreground",
"background",
"p-size",
"p-indent",
"p-line-height",
"a-size",
"pre-size",
"pre-line-height",
"h1-size",
"h2-size",
"h3-size",
"heading-line-height",
"ul-size",
"ul-line-height",
"blockquote-size",
"blockquote-line-height",
"mono",
"serif",
"sans-serif",
].map((key) => {
cli.option(key);
cli.conflicts(key, "body"); cli.conflicts(key, "body");
return key; return key;
}); });
const argv = cli const argv = cli
.conflicts("author", "body") .conflicts("author", "body")
.conflicts("description", "body") .conflicts("descriptions", "body")
.conflicts("html", "body") .conflicts("html", "body")
.group(["html", "body"], "Core:") .group(["html", "body"], "Core:")
.group(["author", "description", "css", "dir"], "HTML:") .group(["author", "descriptions", "css", "dir"], "HTML:")
.group(["image", "audio", "video"], "Inline Media:") .group(["image", "audio", "video"], "Inline Media:")
.group(GMI_CSS_VARS, "gmi.css:") .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;
let styles = ""; if (!argv.html && !argv.body) {
if (argv.css === "gmi.css") {
styles = GMI_CSS_VARS.reduce((style, key) => {
if (argv[key]) {
style += `--${key}: ${argv[key]};`;
}
return style;
}, styles);
argv.css = new CleanCSS().minify(
readFileSync(
// TODO import.meta.resolve is supposed to accomplish this without "path"
path.resolve(
path.dirname(new URL(import.meta.url).pathname),
"./gmi.css"
),
"utf-8"
)
).styles;
} else if (argv.css === "base") {
argv.css = BASE_CSS;
} else {
argv.css = "";
}
if (!argv.lang && !argv.body) {
cli.showHelp(); cli.showHelp();
console.error(`\nMissing required argument: --html or --body`); console.error(`\nMissing required argument: --html or --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 {
gemtext = readFileSync(process.stdin.fd); gemtext = readFileSync(process.stdin.fd, "utf-8");
} catch (e) { } catch (e) {
cli.showHelp(); cli.showHelp();
console.error("\nMissing files: pipe from stdin or provide [files..]"); console.error("\nMissing files: pipe from stdin or provide [files..]");

48
css.js Normal file
View file

@ -0,0 +1,48 @@
import { readFileSync } from "fs";
import path from "path";
import { stringify, parse } from "css";
export { stringify, parse };
// TODO import.meta.resolve is supposed to accomplish this without "path"
const GMI_CSS = path.resolve(
path.dirname(new URL(import.meta.url).pathname),
"./gmi.css"
);
export const CORE = parse(`p,
a,
pre,
h1,
h2,
h3,
ul,
blockquote,
img,
audio,
video {
display: block;
margin: 0;
padding: 0;
overflow-wrap: anywhere;
max-width: 100%;
}`);
export const FULL = parse(readFileSync(GMI_CSS, "utf-8"));
export function rootVariables({ stylesheet }) {
return stylesheet.rules
.find(
({ type, selectors }) => type === "rule" && selectors.includes(":root")
)
.declarations.reduce(
(obj, { property, value }) =>
!/^\-\-/.test(property)
? obj
: Object.assign(obj, { [property.replace("--", "")]: value }),
{}
);
}
export function resolve(arg, options) {
return stringify(parse(readFileSync(path.resolve(arg), "utf-8")), options);
}

View file

@ -48,16 +48,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 \fB--css\fR [\fIMODE\fR|\fIFILE\fR|\fICSS\fR]
.RS 4 .RS 4
By default this will be set to \fBgmi.css\fR. Choosing \fBbase\fR will use just what By default this will be set to \fBfull\fR enabling a handful of customizable
is needed to fix issues with CSS 2.1's Normal Flow. Choosing \fBnone\fR will not variables to be set. See --help for the complete list.
include any style information and inline elements will be wrapped in <p>.
.P
.RE
\fBgmi.css\fR
.RS 4
See --help or gmi.css(5) for the complete list of customizable styles.
.P .P
.RE .RE
.nf .nf
@ -68,7 +62,17 @@ gmi-web --html en \\
.fi .fi
.RE .RE
.P .P
\fB[--image|--audio|--video]\fR \fIEXENSIONS\fR .RS 4
Choosing \fBcore\fR will use just what is needed to fix vertical layout issues
with CSS 2.1's Normal Flow and inline elements.
.P
Pointing to a .css \fIFILE\fR or providing a valid \fICSS\fR string will use those
styles.
.P
Choosing \fBnone\fR will not include any style information.
.P
.RE
\fB[--image|--audio|--video]\fR \fIEXTENSIONS\fR
.RS 4 .RS 4
Include media extensions inline. You can provide multiple extensions per flag Include media extensions inline. You can provide multiple extensions per flag
or multiple flags per extension. or multiple flags per extension.
@ -89,10 +93,6 @@ All the options documented here and by \fB--help\fR may be captured in a .json
file and passed to \fB--config\fR instead of as flags on the command-line. file and passed to \fB--config\fR instead of as flags on the command-line.
.P .P
.RE .RE
.SH SEE ALSO
.P
\fBgmi.css\fR(5)
.P
.SH AUTHORS .SH AUTHORS
.P .P
Maintained by Talon Poole <code@talon.computer>. Up-to-date sources can be Maintained by Talon Poole <code@talon.computer>. Up-to-date sources can be

View file

@ -20,10 +20,6 @@ mobile-friendly fashion!
*--body* *--body*
Generate only the HTML for the lines of the Gemini document. Generate only the HTML for the lines of the Gemini document.
```
gmi-web --body < doc.gmi
```
*--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
to adjust the document text direction from "ltr" to "rtl". to adjust the document text direction from "ltr" to "rtl".
@ -35,13 +31,9 @@ gmi-web --body < doc.gmi
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_ *--css* [_MODE_|_FILE_|_CSS_]
By default this will be set to *gmi.css*. Choosing *base* will use just what By default this will be set to *full* enabling a handful of customizable
is needed to fix issues with CSS 2.1's Normal Flow. Choosing *none* will not variables to be set. See --help for the complete list.
include any style information and inline elements will be wrapped in <p>.
*gmi.css*
See --help or gmi.css(5) for the complete list of customizable styles.
``` ```
gmi-web --html en \\ gmi-web --html en \\
@ -49,7 +41,15 @@ gmi-web --html en \\
--background "#9EEBCF" < doc.gmi --background "#9EEBCF" < doc.gmi
``` ```
*[--image|--audio|--video]* _EXENSIONS_ Choosing *core* will use just what is needed to fix vertical layout issues
with CSS 2.1's Normal Flow and inline elements.
Pointing to a .css _FILE_ or providing a valid _CSS_ string will use those
styles.
Choosing *none* will not include any style information.
*[--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
or multiple flags per extension. or multiple flags per extension.
@ -64,10 +64,6 @@ gmi-web --html en \\
All the options documented here and by *--help* may be captured in a .json All the options documented here and by *--help* may be captured in a .json
file and passed to *--config* instead of as flags on the command-line. file and passed to *--config* instead of as flags on the command-line.
# SEE ALSO
*gmi.css*(5)
# AUTHORS # AUTHORS
Maintained by Talon Poole <code@talon.computer>. Up-to-date sources can be Maintained by Talon Poole <code@talon.computer>. Up-to-date sources can be

17
html.js
View file

@ -33,15 +33,14 @@ ${body(tokens, options)}
`; `;
} }
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) { 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 !== "" ? `<meta name="color-scheme" content="dark light">` : ""} options.css && options.css !== ""
${options.css !== "" ? `<style>${options.css}</style>` : ""} ? `<meta name="color-scheme" content="dark light">`
: ""
}${options.css && options.css !== "" ? `<style>${options.css}</style>` : ""}
<title>${options.title}</title>${ <title>${options.title}</title>${
!options.author ? "" : `<meta name="author" content="${options.author}">` !options.author ? "" : `<meta name="author" content="${options.author}">`
}${ }${
@ -70,17 +69,11 @@ function line(
return `<img src="${href}"${titleProp}/>`; return `<img src="${href}"${titleProp}/>`;
} }
if (audio && matchesExt(href, audio)) { if (audio && matchesExt(href, audio)) {
if (css === "") {
return `<p><audio controls src="${href}"${titleProp}></audio><p>`;
}
return `<audio controls src="${href}"${titleProp}></audio>`; return `<audio controls src="${href}"${titleProp}></audio>`;
} }
if (video && matchesExt(href, video)) if (video && matchesExt(href, video))
return `<video controls src="${href}"${titleProp}/></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>`; return `<a href="${href}">${title ? escape(title) : href}</a>`;
} }

29
package-lock.json generated
View file

@ -25,6 +25,11 @@
"buffer-equal": "^1.0.0" "buffer-equal": "^1.0.0"
} }
}, },
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@ -127,6 +132,21 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
}, },
"css": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
"integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
"requires": {
"inherits": "^2.0.4",
"source-map": "^0.6.1",
"source-map-resolve": "^0.6.0"
}
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"define-properties": { "define-properties": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -549,6 +569,15 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}, },
"source-map-resolve": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
"integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
"requires": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0"
}
},
"stream-shift": { "stream-shift": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",

View file

@ -25,6 +25,7 @@
}, },
"dependencies": { "dependencies": {
"clean-css": "^5.0.1", "clean-css": "^5.0.1",
"css": "^3.0.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"vinyl-fs": "^3.0.3", "vinyl-fs": "^3.0.3",

3
test.css Normal file
View file

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