diff --git a/Makefile b/Makefile index ff64252..2495db5 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,6 @@ example/test.html: example/test.gmi ./test.sh clean: - rm -rf gmi.min.* gmi-web.1 node_modules + rm -rf gmi.min.* gmi-web.1 gmi.css.5 node_modules .PHONY: clean format diff --git a/cli.js b/cli.js index d70e3d0..db9911d 100755 --- a/cli.js +++ b/cli.js @@ -2,8 +2,7 @@ import path from "path"; import fs from "vinyl-fs"; import map from "map-stream"; -import tokenize from "./tokenize.js"; -import toHTML from "./to-html.js"; +import toHTML from "./gmi.html.js"; import yargs from "yargs"; // TODO: automatically pull these in from gmi.css (also for gmi.css(5)) @@ -69,19 +68,18 @@ yargs(process.argv.slice(2)) "gmi.css is included by default. Use --no-css for the bare minimum "; +export const GMI_REGEX = /^((=>\s?(?[^\s]+)(\s(?.+))?)|(?<pre>```\s?(?<alt>.+)?)|(###\s?(?<h3>.+))|(##\s?(?<h2>.+))|(#\s?(?<h1>.+))|(\*\s?(?<li>.+))|(>\s?(?<quote>.+))|(?<text>(.+)?))$/; + +export function html(file, options) { + const tokens = file.contents + .toString("utf8") + .split("\n") + .map((line) => GMI_REGEX.exec(line).groups); + + if (options.body) return body(tokens, options); + + return `<!DOCTYPE html> +<html lang="${options.language}" style="${options.styles}"> +<head>${head( + Object.assign(options, { + title: tokens[0].h1, + charset: "utf-8", + }) + )}</head> +<body>${body(tokens, options)}</body> +</html> +`; +} + +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 const GMI_CSS = fs.readFileSync( - path.resolve(path.dirname(new URL(import.meta.url).pathname), "./gmi.min.css"), + // TODO import.meta.resolve is supposed to accomplish this without "path" + path.resolve( + path.dirname(new URL(import.meta.url).pathname), + "./gmi.min.css" + ), "utf8" ); -export default (options) => (file, cb) => { - if (!TOKENS_EXT.test(file.path)) return cb(null); - file.contents = Buffer.from(toHTML(file, options)); - file.path = file.path.replace(TOKENS_EXT, ".html"); - if (options.verbose) console.log(file.path); - return cb(null, file); -}; - -export function toHTML (file, options) { - const tokens = JSON.parse(file.contents.toString("utf8")); - const title = tokens[0].h1; - return `<!DOCTYPE html> -<html lang="${options.language}" style="${options.override}"> -${head(file.path, title, options)}<body> -${gemtext(tokens, options)} -</body> -</html> -`; -}; - -export function head(file, title, options) { - return `<head> -<meta charset="utf-8"> +export function head(options) { + return ` +<meta name="viewport" content="width=device-width,initial-scale=1"> +<meta charset="${options.charset}"> <meta language="${options.language}"> -<meta name="viewport" content="width=device-width,initial-scale=1">${ +<title>${options.title}${ + !options.description + ? "" + : `` + }${ !options.css - ? BASE_STYLE + ? `` : `\n\n` }${ !options.canonical ? "" : `` } -${title} - `; } -export function gemtext(tokens, options) { - let body = []; +export function body(tokens, options) { + let lines = []; let cursor = tokens.shift(); while (tokens.length) { if (cursor.pre) { - body.push(``); + lines.push(``); const closing = tokens.findIndex((token) => token.pre); - body = body.concat(tokens.slice(0, closing).map(({ text }) => text)); - body.push(""); + lines = lines.concat(tokens.slice(0, closing).map(({ text }) => text)); + lines.push(""); tokens = tokens.slice(closing + 1); } else if (cursor.li) { - body.push(`
    `); + lines.push(`
      `); const closing = tokens.findIndex((token) => !token.li); - body = body + lines = lines .concat([line(cursor)]) .concat(tokens.slice(0, closing).map(line)); - body.push("
    "); + lines.push("
"); tokens = tokens.slice(closing); } else { - body.push(line(cursor, options)); + lines.push(line(cursor, options)); } cursor = tokens.shift(); } - return body.join("\n"); + return lines.join("\n"); } +export const IMAGE_EXT = /\.(apng|avif|gif|jpg|jpeg|jfif|pjpeg|pjp|png|svg|webp)$/; +export const AUDIO_EXT = /\.(mp3|wav|aac|aacp|mpeg|off|flac)$/; +export const VIDEO_EXT = /\.(mp4|webm)$/; + function line( { text, href, title, pre, alt, h1, h2, h3, li, quote }, { images, audio, video } = {} @@ -87,7 +97,7 @@ function line( if (text) return `

${escape(text)}

`; if (href) { const titleProp = title ? ` title="${title}"` : ""; - if (images && IMG_EXT.test(href)) return ``; + if (images && IMAGE_EXT.test(href)) return ``; if (audio && AUDIO_EXT.test(href)) return ``; if (video && VIDEO_EXT.test(href)) @@ -102,3 +112,13 @@ function line( if (quote) return `
${escape(quote)}
`; return `


`; } + +export const GMI_EXT = /\.gmi$/; + +export default (options) => (file, cb) => { + if (!GMI_EXT.test(file.path)) return cb(null); + file.contents = Buffer.from(html(file, options)); + file.path = file.path.replace(GMI_EXT, ".html"); + if (options.verbose) console.log(file.path); + return cb(null, file); +}; diff --git a/package-lock.json b/package-lock.json index 08af3a1..134808a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gmi-web-cli", - "version": "1.0.5-rc.1", + "version": "1.0.7-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tokenize.js b/tokenize.js deleted file mode 100644 index 2477b4a..0000000 --- a/tokenize.js +++ /dev/null @@ -1,14 +0,0 @@ -export const GMI_REGEX = /^((=>\s?(?[^\s]+)(\s(?.+))?)|(?<pre>```\s?(?<alt>.+)?)|(###\s?(?<h3>.+))|(##\s?(?<h2>.+))|(#\s?(?<h1>.+))|(\*\s?(?<li>.+))|(>\s?(?<quote>.+))|(?<text>(.+)?))$/; -export const GMI_EXT = /\.gmi$/; -export const tokenize = (gemtext) => - JSON.stringify( - gemtext.split("\n").map((line) => GMI_REGEX.exec(line).groups), - null, - 2 - ); -export default (file, cb) => { - if (!GMI_EXT.test(file.path)) return cb(null, file); - file.contents = Buffer.from(tokenize(file.contents.toString("utf8"))); - file.path = file.path.replace(GMI_EXT, ".tokens.json"); - cb(null, file); -};