const gulp = require("gulp") const { task, src, dest } = gulp const { parallel, series } = gulp const parcel = require("gulp-parcel") const replace = require("gulp-replace") const rename = require("gulp-rename") const eslint = require("gulp-eslint") const file = require("gulp-file") const path = require("path") const del = require("del") const platforms = require("platform-folders") const fs = require("fs").promises const fetch = require("node-fetch") const http = require("http") const { COPYFILE_EXCL } = require("fs").constants const { URL } = require("url") let srcFilesLoaded = false let compl let conf let keys let util const requireSrcFiles = () => { if (srcFilesLoaded) { return } compl = require("./completions") // eslint-disable-line global-require conf = require("./conf") // eslint-disable-line global-require keys = require("./keys") // eslint-disable-line global-require util = require("./util") // eslint-disable-line global-require srcFilesLoaded = true } const paths = { scripts: ["conf.priv.js", "completions.js", "conf.js", "actions.js", "help.js", "keys.js", "util.js"], entry: "conf.js", gulpfile: "gulpfile.js", readme: "README.tmpl.md", assets: "assets", screenshots: "assets/screenshots", favicons: "assets/favicons", readmeOut: "README.md", scriptOut: "surfingkeys.js", installDir: platforms.getConfigHome(), } const servePort = 9919 // This notice will be injected into the generated README.md file const disclaimer = `\ ` task("clean", () => del(["build", ".cache", ".tmp-gulp-compile-*"])) task("clean-favicons", () => del([paths.favicons])) task("lint", () => gulp .src([...paths.scripts, paths.gulpfile]) .pipe(eslint()) .pipe(eslint.format())) task("check-priv", async () => { try { await fs.stat("./conf.priv.js") } catch (e) { // eslint-disable-next-line no-console console.log("Notice: Initializing ./conf.priv.js - configure your API keys here.") return fs.copyFile("./conf.priv.example.js", "./conf.priv.js", COPYFILE_EXCL) } return Promise.resolve() }) task("docs", parallel(async () => { requireSrcFiles() const screens = {} let screenshotList = "" const screenshots = await fs.readdir(path.join(__dirname, paths.screenshots)) screenshots.forEach((s) => { const name = path.basename(s, ".png").split("-") const alias = name[0] if (!screens[alias]) { screens[alias] = [] } screens[alias].push(path.join(paths.screenshots, path.basename(s))) }) let complTable = Object.keys(compl).sort((a, b) => { if (a < b) return -1 if (a > b) return 1 return 0 }) let keysTable = Object.keys(keys.maps).sort((a, b) => { if (a === "global") return -1 if (b === "global") return 1 if (a < b) return -1 if (a > b) return 1 return 0 }) complTable = await complTable.reduce(async (acc1p, k) => { const acc1 = await acc1p const c = compl[k] const u = new URL(c.domain ? `https://${c.domain}` : c.search) const domain = u.hostname let s = "" if (screens[c.alias]) { screens[c.alias].forEach((url, i) => { const num = (i > 0) ? ` ${i + 1}` : "" s += `:framed_picture:` screenshotList += `##### ${c.name}${num}\n` screenshotList += `![${c.name} screenshot](./${url})\n\n` }) } const faviconExt = c.favicon ? path.extname(new URL(c.favicon).pathname) : ".ico" const favicon = ` ` return `${acc1} ${favicon} ${c.alias} ${c.name} ${domain} ${s} ` }, Promise.resolve("")) keysTable = await keysTable.reduce(async (acc1p, domain) => { const acc1 = await acc1p const header = "MappingDescription" const c = keys.maps[domain] const maps = c.reduce((acc2, map) => { let leader = "" if (typeof map.leader !== "undefined") { leader = map.leader // eslint-disable-line prefer-destructuring } else if (domain === "global") { leader = "" } else { leader = conf.siteleader } const mapStr = util.escape(`${leader}${map.alias}`.replace(" ", "")) return `${acc2}${mapStr}${map.description}\n` }, "") let domainStr = "global" const favicon = ` ` if (domain !== "global") { domainStr = `${favicon}${domain}` } return `${acc1}${domainStr}${header}\n${maps}` }, Promise.resolve("")) return src([paths.readme]) .pipe(replace("", disclaimer)) .pipe(replace("", Object.keys(compl).length)) .pipe(replace("", complTable)) .pipe(replace("", Object.keys(keys.maps).reduce((acc, m) => acc + m.length, 0))) .pipe(replace("", Object.keys(keys.maps).length)) .pipe(replace("", keysTable)) .pipe(replace("", screenshotList)) .pipe(rename(paths.readmeOut)) .pipe(dest(".")) })) const getFavicon = async ({ domain, favicon }, timeout = 5000) => { const url = favicon let data const ext = path.extname(new URL(favicon).pathname) try { const res = await fetch(url, { timeout }) if (!res.ok) { throw new Error(`request to ${url} failed with code ${res.status}`) } data = await res.buffer() } catch (e) { process.stdout.write(`no favicon found for ${url}: ${e}\n`) // transparent pixel data = Buffer.from( "AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA==", "base64" ) } return { name: `${domain}${ext}`, source: data, } } task("favicons", series("clean-favicons", async () => { requireSrcFiles() const sites = [].concat( // search engine completions Object.entries(compl) .map(([, v]) => ({ domain: new URL(v.domain ? `https://${v.domain}` : v.search).hostname, favicon: v.favicon ? v.favicon : `${new URL(v.domain ? `https://${v.domain}` : v.search).origin}/favicon.ico`, })), // site-specific keybindings Object.keys(keys.maps) .filter(k => k !== "global") .map(k => ({ domain: k, favicon: `${new URL(`https://${k}`).origin}/favicon.ico`, })), ).filter((e, i, arr) => i === arr.indexOf(e)) // Keep only first occurrence of each element const favicons = (await Promise.all(sites.map(async site => getFavicon(site)))) .filter(e => e !== undefined) return file(favicons, { src: true }) .pipe(dest(paths.favicons)) })) task("docs-full", parallel("docs", "favicons")) task("build", series( parallel( "check-priv", "clean", ), parallel( "lint", () => src(paths.entry, { read: false }) .pipe(parcel()) .pipe(rename(paths.scriptOut)) .pipe(dest("build")), ) )) task("dist", parallel("docs-full", "build")) task("install", series("build", () => src(path.join("build", paths.scriptOut)) .pipe(dest(paths.installDir)))) const watch = (g, t) => () => gulp.watch(g, { ignoreInitial: false, usePolling: true }, t) task("watch-build", watch(paths.scripts, series("build"))) task("watch-install", watch(paths.scripts, series("install"))) task("watch-lint", watch([...paths.scripts, paths.gulpfile], series("lint"))) task("watch-docs", watch([...paths.scripts, paths.readme], series("docs"))) task("watch-docs-full", watch([...paths.scripts, paths.readme], series("docs-full"))) const serve = (done) => { const srv = http.createServer(async (req, res) => { console.log(`${new Date().toISOString()} ${req.method} ${req.url}`) // eslint-disable-line no-console res.writeHead(200, { "Content-Type": "text/javascript; charset=UTF-8", }) res.end(await fs.readFile(path.join("build", paths.scriptOut))) }) srv.listen(servePort) console.log(`web server is listening on port ${servePort}`) // eslint-disable-line no-console srv.on("close", () => { console.log("web server is closing...") // eslint-disable-line no-console done() }) } task("serve-simple", serve) task("serve-build", parallel("watch-build", "serve-simple")) task("serve", series("serve-build")) task("watch", series("watch-install")) task("default", series("build"))