add --inline
refactor css.js and html.js
This commit is contained in:
parent
232dbe13eb
commit
b172d16ca9
|
@ -141,7 +141,7 @@ gmi-web --config web.json $(find ~/gmi/dst -name '*.gmi')
|
||||||
|
|
||||||
# gmi.css
|
# gmi.css
|
||||||
|
|
||||||
The CSS variables exposed by gmi-web(1) are derived from [gmi.css](./gmi.css), which could be used on its own. The default values are optimized for readability and mobile-friendliness and may be customized by adding a style property to `<html>`.
|
The CSS variables exposed by gmi-web(1) are derived from [gmi.css](./gmi.css), which could be used independently. The default values are optimized for readability and mobile-friendliness and may be customized by adding a style property to `<html>`.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<head>
|
<head>
|
||||||
|
|
9
cli.js
9
cli.js
|
@ -47,6 +47,9 @@ const cli = yargs(process.argv.slice(2))
|
||||||
default: "full",
|
default: "full",
|
||||||
requiresArg: true,
|
requiresArg: true,
|
||||||
},
|
},
|
||||||
|
inline: {
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
image: {
|
image: {
|
||||||
type: "array",
|
type: "array",
|
||||||
requiresArg: true,
|
requiresArg: true,
|
||||||
|
@ -92,6 +95,12 @@ const argv = cli
|
||||||
.showHelpOnFail(true)
|
.showHelpOnFail(true)
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|
||||||
|
if (argv.inline && argv.css === "full") {
|
||||||
|
cli.showHelp();
|
||||||
|
console.error(`\n--inline is not compatible with --css full`);
|
||||||
|
cli.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
if (!argv.html && !argv.body) {
|
if (!argv.html && !argv.body) {
|
||||||
cli.showHelp();
|
cli.showHelp();
|
||||||
console.error(`\nMissing required argument: --html or --body`);
|
console.error(`\nMissing required argument: --html or --body`);
|
||||||
|
|
88
css.js
88
css.js
|
@ -3,31 +3,28 @@ import path from "path";
|
||||||
import { stringify, parse } from "css";
|
import { stringify, parse } from "css";
|
||||||
export { stringify, parse };
|
export { stringify, parse };
|
||||||
|
|
||||||
// TODO import.meta.resolve is supposed to accomplish this without "path"
|
export const CORE = parse(`p, pre, ul, blockquote, h1, h2, h3 {
|
||||||
const GMI_CSS = path.resolve(
|
margin-top: 0;
|
||||||
path.dirname(new URL(import.meta.url).pathname),
|
margin-bottom: 0;
|
||||||
"./gmi.css"
|
overflow-wrap: break-word;
|
||||||
);
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
export const CORE = parse(`p,
|
a { display: block; }
|
||||||
a,
|
|
||||||
pre,
|
img, audio, video {
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
ul,
|
|
||||||
blockquote,
|
|
||||||
img,
|
|
||||||
audio,
|
|
||||||
video {
|
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}`);
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
export const FULL = parse(readFileSync(GMI_CSS, "utf-8"));
|
// TODO import.meta.resolve is supposed to accomplish this without "path"
|
||||||
|
export const FULL = parse(
|
||||||
|
readFileSync(
|
||||||
|
path.resolve(path.dirname(new URL(import.meta.url).pathname), "./gmi.css"),
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
export function rootVariables({ stylesheet }) {
|
export function rootVariables({ stylesheet }) {
|
||||||
return stylesheet.rules
|
return stylesheet.rules
|
||||||
|
@ -43,6 +40,51 @@ export function rootVariables({ stylesheet }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolve(arg, options) {
|
export function override(options) {
|
||||||
return stringify(parse(readFileSync(path.resolve(arg), "utf-8")), options);
|
if (options.css !== "full") return "";
|
||||||
|
const vars = rootVariables(FULL);
|
||||||
|
|
||||||
|
return Object.keys(vars).reduce((styles, key) => {
|
||||||
|
if (typeof options[key] !== undefined && options[key] !== vars[key]) {
|
||||||
|
let value = options[key];
|
||||||
|
if (["a-prefix", "ul-bullet"].includes(key) && value !== "none") {
|
||||||
|
value = `"${value}"`;
|
||||||
|
}
|
||||||
|
styles += `--${key}:${value};`;
|
||||||
|
}
|
||||||
|
return styles;
|
||||||
|
}, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function style({ css }) {
|
||||||
|
if (css === "none") return "";
|
||||||
|
if (css === "core")
|
||||||
|
return `<style>${stringify(CORE, { compress: true })}</style>`;
|
||||||
|
if (css === "full")
|
||||||
|
return `<style>${stringify(FULL, { compress: true })}</style>`;
|
||||||
|
|
||||||
|
return `<style>${stringify(resolve(css, { compress: true }))}</style>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inline(tag, css) {
|
||||||
|
const { stylesheet } =
|
||||||
|
css === "full" ? FULL : css === "core" ? CORE : resolve(css);
|
||||||
|
const styles = stylesheet.rules
|
||||||
|
.filter(({ type, selectors }) => type === "rule" && selectors.includes(tag))
|
||||||
|
.reduce((style, { declarations }) => {
|
||||||
|
declarations.forEach(({ property, value }) => {
|
||||||
|
style = `${style}${property}:${value};`;
|
||||||
|
});
|
||||||
|
return style;
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
return styles !== "" ? ` style="${styles}"` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function load(file, options) {
|
||||||
|
if (fs.existsSync(mode)) {
|
||||||
|
return parse(readFileSync(path.resolve(file), "utf-8"));
|
||||||
|
} else {
|
||||||
|
throw new Error(`Cannot find file ${mode}.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ gmi-web - A bridge between Gemini and HTML
|
||||||
|
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
|
|
||||||
Convert Gemtext to semantic HTML styled in a readable, predictable and
|
Convert Gemtext to semantic HTML styled in a readable and mobile-friendly
|
||||||
mobile-friendly fashion!
|
fashion!
|
||||||
|
|
||||||
# OPTIONS
|
# OPTIONS
|
||||||
|
|
||||||
|
@ -24,12 +24,12 @@ mobile-friendly fashion!
|
||||||
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".
|
||||||
|
|
||||||
|
Use *--author* _NAME_ to set the author <meta> tag on every file.
|
||||||
|
|
||||||
Use *--descriptions* _LIMIT_ to apply the first non-empty text line of each
|
Use *--descriptions* _LIMIT_ to apply the first non-empty text line of each
|
||||||
file as the description <meta> tag. _LIMIT_ will be used to truncate the text
|
file as the description <meta> tag. _LIMIT_ will be used to truncate the text
|
||||||
with an ellipsis at that number of characters.
|
with an ellipsis at that number of characters.
|
||||||
|
|
||||||
Use *--author* _NAME_ to set the author <meta> tag on every file.
|
|
||||||
|
|
||||||
*--css* [_MODE_|_FILE_]
|
*--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. See *--help* for the complete list.
|
variables. See *--help* for the complete list.
|
||||||
|
@ -41,11 +41,15 @@ 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. Pointing to a .css _FILE_
|
with CSS 2.1's Normal Flow and inline elements. Choosing *none* will not
|
||||||
will use those styles.
|
include any style information.
|
||||||
|
|
||||||
Choosing *none* will not include any style information including when paired
|
*--inline* will insert the declarations as "style" properties on their
|
||||||
with *--body* where it will not apply the core inline styles.
|
respective tags. This is the sole behavior when using *--body* and can be
|
||||||
|
turned off by using --css *none*. When using --css *full* this feature is
|
||||||
|
unavailable.
|
||||||
|
|
||||||
|
Pointing to a .css _FILE_ will use those styles and also works with *--inline*
|
||||||
|
|
||||||
*[--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
|
||||||
|
|
45
gmi.css
45
gmi.css
|
@ -1,3 +1,30 @@
|
||||||
|
p,
|
||||||
|
pre,
|
||||||
|
ul,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--body-width: 48rem;
|
--body-width: 48rem;
|
||||||
--serif: georgia, times, serif;
|
--serif: georgia, times, serif;
|
||||||
|
@ -36,24 +63,6 @@ body {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
p,
|
|
||||||
a,
|
|
||||||
pre,
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
ul,
|
|
||||||
blockquote,
|
|
||||||
img,
|
|
||||||
audio,
|
|
||||||
video {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3 {
|
h3 {
|
||||||
|
|
150
html.js
150
html.js
|
@ -8,6 +8,11 @@ export const GMI_REGEX = /^((=>\s?(?<href>[^\s]+)(\s(?<title>.+))?)|(?<pre>```\s
|
||||||
export function toHTML(gemtext, options) {
|
export function toHTML(gemtext, options) {
|
||||||
const tokens = gemtext.split("\n").map((line) => GMI_REGEX.exec(line).groups);
|
const tokens = gemtext.split("\n").map((line) => GMI_REGEX.exec(line).groups);
|
||||||
|
|
||||||
|
if (options.body) return body(tokens, options);
|
||||||
|
|
||||||
|
const truncate = (text, limit) =>
|
||||||
|
text.length > limit ? `${text.substring(0, limit)}...` : text;
|
||||||
|
|
||||||
let description =
|
let description =
|
||||||
options.descriptions > 0
|
options.descriptions > 0
|
||||||
? tokens.find((token) => {
|
? tokens.find((token) => {
|
||||||
|
@ -15,31 +20,10 @@ export function toHTML(gemtext, options) {
|
||||||
})
|
})
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
if (options.body) return body(tokens, options);
|
|
||||||
|
|
||||||
const truncate = (text, limit) =>
|
|
||||||
text.length > limit ? `${text.substring(0, limit)}...` : text;
|
|
||||||
|
|
||||||
function overrideStyles(options) {
|
|
||||||
if (options.css !== "full") return "";
|
|
||||||
const vars = CSS.rootVariables(CSS.FULL);
|
|
||||||
|
|
||||||
return Object.keys(vars).reduce((styles, key) => {
|
|
||||||
if (typeof options[key] !== undefined && options[key] !== vars[key]) {
|
|
||||||
let value = options[key];
|
|
||||||
if (["a-prefix", "ul-bullet"].includes(key) && value !== "none") {
|
|
||||||
value = `"${value}"`;
|
|
||||||
}
|
|
||||||
styles += `--${key}:${value};`;
|
|
||||||
}
|
|
||||||
return styles;
|
|
||||||
}, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="${options.language}" dir="${options.dir}" style='${overrideStyles(
|
<html lang="${options.language}" dir="${options.dir}" style='${
|
||||||
options
|
options.inline ? CSS.inline("html", options.css) : CSS.override(options)
|
||||||
)}'>
|
}'>
|
||||||
<head>${head(
|
<head>${head(
|
||||||
Object.assign(options, {
|
Object.assign(options, {
|
||||||
title: tokens[0].h1,
|
title: tokens[0].h1,
|
||||||
|
@ -47,7 +31,7 @@ export function toHTML(gemtext, options) {
|
||||||
description && truncate(description.text, options.descriptions),
|
description && truncate(description.text, options.descriptions),
|
||||||
})
|
})
|
||||||
)}</head>
|
)}</head>
|
||||||
<body>
|
<body${options.inline ? CSS.inline("body", options.css) : ""}>
|
||||||
${body(tokens, options)}
|
${body(tokens, options)}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -62,7 +46,7 @@ ${
|
||||||
options.schemes || options.css === "full"
|
options.schemes || options.css === "full"
|
||||||
? `<meta name="color-scheme" content="dark light">\n`
|
? `<meta name="color-scheme" content="dark light">\n`
|
||||||
: ""
|
: ""
|
||||||
}${style(options.css)}
|
}${!options.inline ? CSS.style(options) : ""}
|
||||||
<title>${options.title}</title>${
|
<title>${options.title}</title>${
|
||||||
!options.author ? "" : `\n<meta name="author" content="${options.author}">`
|
!options.author ? "" : `\n<meta name="author" content="${options.author}">`
|
||||||
}${
|
}${
|
||||||
|
@ -77,83 +61,95 @@ ${
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function style(mode) {
|
function block(
|
||||||
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 },
|
{ text, href, title, pre, alt, h1, h2, h3, li, quote },
|
||||||
{ image, audio, video, css, body } = {}
|
{ image, audio, video, css, body, inline } = {}
|
||||||
) {
|
) {
|
||||||
if (text) return `<p>${escape(text)}</p>`;
|
let type = "p";
|
||||||
|
let props = "";
|
||||||
|
let content = "<br>";
|
||||||
|
if (text && 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) {
|
if (href) {
|
||||||
const titleProp = title ? ` title="${title}"` : "";
|
|
||||||
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));
|
||||||
|
|
||||||
if (image && matchesExt(href, image)) {
|
if (image && matchesExt(href, image)) {
|
||||||
return `<img src="${href}"${titleProp}${FIX_NORMAL_FLOW}/>`;
|
type = "img";
|
||||||
|
props += ` src=${href}` + alt ? ` title=${alt}` : "";
|
||||||
|
} else if (audio && matchesExt(href, audio)) {
|
||||||
|
type = "audio";
|
||||||
|
props += ` src=${href}` + alt ? ` title=${alt}` : "";
|
||||||
|
} else if (video && matchesExt(href, video)) {
|
||||||
|
type = "video";
|
||||||
|
props += ` src=${href}` + alt ? ` title=${alt}` : "";
|
||||||
|
} else {
|
||||||
|
type = "a";
|
||||||
|
content = title || href;
|
||||||
|
props += ` href=${href}`;
|
||||||
}
|
}
|
||||||
if (audio && matchesExt(href, audio)) {
|
|
||||||
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>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<a href="${href}"${FIX_NORMAL_FLOW}>${
|
|
||||||
title ? escape(title) : href
|
|
||||||
}</a>`;
|
|
||||||
}
|
}
|
||||||
|
if (body || inline) props += CSS.inline(type, css);
|
||||||
|
|
||||||
if (h1) return `<h1>${escape(h1)}</h1>`;
|
return `<${type}${props}>${
|
||||||
if (h2) return `<h2>${escape(h2)}</h2>`;
|
content !== "<br>" ? escape(content) : content
|
||||||
if (h3) return `<h3>${escape(h3)}</h3>`;
|
}</${type}>`;
|
||||||
if (li) return `<li>${escape(li)}</li>`;
|
|
||||||
|
|
||||||
if (quote) return `<blockquote>${escape(quote)}</blockquote>`;
|
|
||||||
|
|
||||||
return `<p><br></p>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function body(tokens, options) {
|
export function body(tokens, options) {
|
||||||
let lines = [];
|
let blocks = [];
|
||||||
|
|
||||||
let cursor = tokens.shift();
|
let cursor = tokens.shift();
|
||||||
while (tokens.length) {
|
while (tokens.length) {
|
||||||
if (cursor.pre) {
|
if (cursor.pre) {
|
||||||
lines.push(`<pre${cursor.alt ? ` title="${cursor.alt}"` : ""}>`);
|
blocks.push(
|
||||||
|
`<pre${cursor.alt ? ` title="${cursor.alt}"` : ""}${
|
||||||
|
options.body || options.inline ? CSS.inline("ul", options.css) : ""
|
||||||
|
}>`
|
||||||
|
);
|
||||||
const closing = tokens.findIndex((token) => token.pre);
|
const closing = tokens.findIndex((token) => token.pre);
|
||||||
lines = lines.concat(tokens.slice(0, closing).map(({ text }) => text));
|
blocks = blocks.concat(tokens.slice(0, closing).map(({ text }) => text));
|
||||||
lines.push("</pre>");
|
blocks.push("</pre>");
|
||||||
tokens = tokens.slice(closing + 1);
|
tokens = tokens.slice(closing + 1);
|
||||||
} else if (cursor.li) {
|
} else if (cursor.li) {
|
||||||
lines.push(`<ul>`);
|
blocks.push(
|
||||||
|
`<ul${
|
||||||
|
options.body || options.inline ? CSS.inline("ul", options.css) : ""
|
||||||
|
}>`
|
||||||
|
);
|
||||||
const closing = tokens.findIndex((token) => !token.li);
|
const closing = tokens.findIndex((token) => !token.li);
|
||||||
lines = lines
|
blocks = blocks
|
||||||
.concat([line(cursor)])
|
.concat([block(cursor)])
|
||||||
.concat(tokens.slice(0, closing).map(line));
|
.concat(tokens.slice(0, closing).map(block));
|
||||||
lines.push("</ul>");
|
blocks.push("</ul>");
|
||||||
tokens = tokens.slice(closing);
|
tokens = tokens.slice(closing);
|
||||||
} else {
|
} else {
|
||||||
lines.push(line(cursor, options));
|
blocks.push(block(cursor, options));
|
||||||
}
|
}
|
||||||
cursor = tokens.shift();
|
cursor = tokens.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.join("\n");
|
return blocks.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GMI_EXT = /\.gmi$/;
|
export const GMI_EXT = /\.gmi$/;
|
||||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "gmi-web-cli",
|
"name": "gmi-web-cli",
|
||||||
"version": "1.0.8-rc.2",
|
"version": "1.0.10-rc.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
Loading…
Reference in a new issue