From b172d16ca99d1122309105e027f70c5f418b0972 Mon Sep 17 00:00:00 2001 From: Talon Poole Date: Thu, 18 Feb 2021 16:43:25 +0000 Subject: [PATCH] add --inline refactor css.js and html.js --- README.md | 2 +- cli.js | 9 +++ css.js | 88 ++++++++++++++++++++------- gmi-web.1.scd | 20 ++++--- gmi.css | 45 ++++++++------ html.js | 150 ++++++++++++++++++++++------------------------ package-lock.json | 2 +- 7 files changed, 188 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index fd30669..79d7479 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ gmi-web --config web.json $(find ~/gmi/dst -name '*.gmi') # 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 ``. +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 diff --git a/cli.js b/cli.js index 91147e3..6cc9a04 100755 --- a/cli.js +++ b/cli.js @@ -47,6 +47,9 @@ const cli = yargs(process.argv.slice(2)) default: "full", requiresArg: true, }, + inline: { + type: "boolean", + }, image: { type: "array", requiresArg: true, @@ -92,6 +95,12 @@ const argv = cli .showHelpOnFail(true) .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) { cli.showHelp(); console.error(`\nMissing required argument: --html or --body`); diff --git a/css.js b/css.js index 7f701d0..610d66b 100644 --- a/css.js +++ b/css.js @@ -3,31 +3,28 @@ 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, pre, ul, blockquote, h1, h2, h3 { + margin-top: 0; + margin-bottom: 0; + overflow-wrap: break-word; + hyphens: auto; +} -export const CORE = parse(`p, -a, -pre, -h1, -h2, -h3, -ul, -blockquote, -img, -audio, -video { +a { display: block; } + +img, audio, video { display: block; - margin: 0; - padding: 0; - overflow-wrap: anywhere; 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 }) { return stylesheet.rules @@ -43,6 +40,51 @@ export function rootVariables({ stylesheet }) { ); } -export function resolve(arg, options) { - return stringify(parse(readFileSync(path.resolve(arg), "utf-8")), options); +export function override(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 ``; + if (css === "full") + return ``; + + return ``; +} + +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}.`); + } } diff --git a/gmi-web.1.scd b/gmi-web.1.scd index 7094b8f..3245f92 100644 --- a/gmi-web.1.scd +++ b/gmi-web.1.scd @@ -12,8 +12,8 @@ gmi-web - A bridge between Gemini and HTML # DESCRIPTION -Convert Gemtext to semantic HTML styled in a readable, predictable and -mobile-friendly fashion! +Convert Gemtext to semantic HTML styled in a readable and mobile-friendly +fashion! # OPTIONS @@ -24,12 +24,12 @@ mobile-friendly fashion! Generate a full HTML5 document with the provided _LANG_. *--dir* can be used to adjust the document text direction from "ltr" to "rtl". + Use *--author* _NAME_ to set the author tag on every file. + Use *--descriptions* _LIMIT_ to apply the first non-empty text line of each file as the description tag. _LIMIT_ will be used to truncate the text with an ellipsis at that number of characters. - Use *--author* _NAME_ to set the author tag on every file. - *--css* [_MODE_|_FILE_] By default this will be set to *full* enabling a handful of customizable 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 - with CSS 2.1's Normal Flow and inline elements. Pointing to a .css _FILE_ - will use those styles. + with CSS 2.1's Normal Flow and inline elements. Choosing *none* will not + include any style information. - Choosing *none* will not include any style information including when paired - with *--body* where it will not apply the core inline styles. + *--inline* will insert the declarations as "style" properties on their + 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_ Include media extensions inline. You can provide multiple extensions per flag diff --git a/gmi.css b/gmi.css index a1d8be7..8f2f3cf 100644 --- a/gmi.css +++ b/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 { --body-width: 48rem; --serif: georgia, times, serif; @@ -36,24 +63,6 @@ body { 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, h2, h3 { diff --git a/html.js b/html.js index 4306447..a04f881 100644 --- a/html.js +++ b/html.js @@ -8,6 +8,11 @@ export const GMI_REGEX = /^((=>\s?(?[^\s]+)(\s(?.+))?)|(?<pre>```\s export function toHTML(gemtext, options) { 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 = options.descriptions > 0 ? tokens.find((token) => { @@ -15,31 +20,10 @@ export function toHTML(gemtext, options) { }) : 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> -<html lang="${options.language}" dir="${options.dir}" style='${overrideStyles( - options - )}'> +<html lang="${options.language}" dir="${options.dir}" style='${ + options.inline ? CSS.inline("html", options.css) : CSS.override(options) + }'> <head>${head( Object.assign(options, { title: tokens[0].h1, @@ -47,7 +31,7 @@ export function toHTML(gemtext, options) { description && truncate(description.text, options.descriptions), }) )}</head> -<body> +<body${options.inline ? CSS.inline("body", options.css) : ""}> ${body(tokens, options)} </body> </html> @@ -62,7 +46,7 @@ ${ options.schemes || options.css === "full" ? `<meta name="color-scheme" content="dark light">\n` : "" -}${style(options.css)} +}${!options.inline ? CSS.style(options) : ""} <title>${options.title}${ !options.author ? "" : `\n` }${ @@ -77,83 +61,95 @@ ${ `; } -export function style(mode) { - if (mode === "none") return ""; - if (mode === "core") - return ``; - if (mode === "full") - return ``; - if (fs.existsSync(mode)) { - return ``; - } else { - throw new Error(`Cannot find file ${mode}.`); - } -} - -function line( +function block( { text, href, title, pre, alt, h1, h2, h3, li, quote }, - { image, audio, video, css, body } = {} + { image, audio, video, css, body, inline } = {} ) { - if (text) return `

${escape(text)}

`; - + let type = "p"; + let props = ""; + let content = "
"; + 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) { - const titleProp = title ? ` title="${title}"` : ""; - const FIX_NORMAL_FLOW = - body && css !== "none" ? ` style="display: block; max-width: 100%;"` : ""; const matchesExt = (url, exts) => exts.some((ext) => new RegExp(`\.${ext}$`).test(url)); - if (image && matchesExt(href, image)) { - return ``; + 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 ``; - } - if (video && matchesExt(href, video)) { - return ``; - } - - return `${ - title ? escape(title) : href - }`; } + if (body || inline) props += CSS.inline(type, css); - if (h1) return `

${escape(h1)}

`; - if (h2) return `

${escape(h2)}

`; - if (h3) return `

${escape(h3)}

`; - if (li) return `
  • ${escape(li)}
  • `; - - if (quote) return `
    ${escape(quote)}
    `; - - return `


    `; + return `<${type}${props}>${ + content !== "
    " ? escape(content) : content + }`; } export function body(tokens, options) { - let lines = []; + let blocks = []; let cursor = tokens.shift(); while (tokens.length) { if (cursor.pre) { - lines.push(``); + blocks.push( + `` + ); const closing = tokens.findIndex((token) => token.pre); - lines = lines.concat(tokens.slice(0, closing).map(({ text }) => text)); - lines.push(""); + blocks = blocks.concat(tokens.slice(0, closing).map(({ text }) => text)); + blocks.push(""); tokens = tokens.slice(closing + 1); } else if (cursor.li) { - lines.push(`
      `); + blocks.push( + `` + ); const closing = tokens.findIndex((token) => !token.li); - lines = lines - .concat([line(cursor)]) - .concat(tokens.slice(0, closing).map(line)); - lines.push("
    "); + blocks = blocks + .concat([block(cursor)]) + .concat(tokens.slice(0, closing).map(block)); + blocks.push(""); tokens = tokens.slice(closing); } else { - lines.push(line(cursor, options)); + blocks.push(block(cursor, options)); } cursor = tokens.shift(); } - return lines.join("\n"); + return blocks.join("\n"); } export const GMI_EXT = /\.gmi$/; diff --git a/package-lock.json b/package-lock.json index cc9b396..ee84e59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gmi-web-cli", - "version": "1.0.8-rc.2", + "version": "1.0.10-rc.2", "lockfileVersion": 1, "requires": true, "dependencies": {