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 express = require("express") const gulpIf = require("gulp-if") const { COPYFILE_EXCL } = require("fs").constants const { URL } = require("url") const pkg = require("./package.json") const copyrightYearOne = 2017 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", faviconsManifest: "favicons.json", readmeOut: "README.md", scriptOut: "surfingkeys.js", installDir: platforms.getConfigHome(), } let srcFilesLoaded = false let compl let conf let keys let util const requireSrcFiles = () => { if (srcFilesLoaded) { return } /* eslint-disable global-require, import/no-dynamic-require */ compl = require("./completions") conf = require("./conf") keys = require("./keys") util = require("./util") /* eslint-enable global-require, import/no-dynamic-require */ srcFilesLoaded = true } let faviconsManifest const loadFaviconsManifest = async () => { if (typeof faviconsManifest === "object" && Object.keys(faviconsManifest).length > 0) { return } try { // eslint-disable-next-line global-require, import/no-dynamic-require faviconsManifest = require(`./${path.join(paths.favicons, paths.faviconsManifest)}`) } catch (e) { console.log(`Warning: couldn't load favicons manifest: ${e}`) // eslint-disable-line no-console faviconsManifest = {} } } 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])) const lint = (globs, opts = {}) => gulp.src(globs) .pipe(eslint(opts)) .pipe(eslint.format()) task("lint", () => lint([...paths.scripts, paths.gulpfile])) task("lint-fix", () => lint([...paths.scripts, paths.gulpfile], { fix: true }) .pipe(gulpIf( (f) => f.eslint !== undefined && f.eslint.fixed === true, gulp.dest((f) => f.base), ))) 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() }) const parseContributor = (contributor) => { let c = contributor if (typeof contributor === "string") { const m = contributor.match(/^(?.*?)\s*(<(?.*?)>)?\s*(\((?.*?)\))?$/) if (!m) { throw new Error(`couldn't parse contributor '${contributor}'`) } c = m.groups } else if (typeof contributor !== "object") { throw new Error(`expected contributor to be of type 'string' or 'object', got '${typeof contributor}'`) } if (!c.name) { return null } return `${c.url ? `` : ""}${c.name}${c.url ? "" : ""}` } task("docs", parallel(async () => { requireSrcFiles() await loadFaviconsManifest() 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 favicon = faviconsManifest[domain] ? ` ` : "" 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" let favicon = "" if (domain !== "global") { favicon = faviconsManifest[domain] ? ` ` : "" domainStr = `${favicon}${domain}` } return `${acc1}${domainStr}${header}\n${maps}` }, Promise.resolve("")) const year = (new Date()).getFullYear() const copyrightYears = `${copyrightYearOne !== year ? `${copyrightYearOne}-${year}` : copyrightYearOne}` let copyright = `

Author

© ${copyrightYears} ${parseContributor(pkg.author)}

` if (Array.isArray(pkg.contributors) && pkg.contributors.length > 0) { copyright += "

Contributors

    " copyright += pkg.contributors.reduce((acc, c) => `${acc}
  • ${parseContributor(c)}
  • `, "") copyright += "

" } copyright += `

License

Released under the ${pkg.license} License

` return src([paths.readme]) .pipe(replace("", disclaimer)) .pipe(replace("", Object.keys(compl).length)) .pipe(replace("", complTable)) .pipe(replace("", Object.values(keys.maps).reduce((acc, m) => acc + m.length, 0))) .pipe(replace("", Object.keys(keys.maps).length)) .pipe(replace("", keysTable)) .pipe(replace("", screenshotList)) .pipe(replace("", copyright)) .pipe(rename(paths.readmeOut)) .pipe(dest(".")) })) const getFavicon = async ({ domain, favicon }, timeout = 5000) => { const url = favicon let data 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 { domain, name: `${domain}.ico`, 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: `https://icons.duckduckgo.com/ip3/${new URL(v.domain ? `https://${v.domain}` : v.search).hostname}.ico`, })), // site-specific keybindings Object.keys(keys.maps) .filter((k) => k !== "global") .map((k) => ({ domain: k, favicon: `https://icons.duckduckgo.com/ip3/${new URL(`https://${k}`).hostname}.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) faviconsManifest = favicons.reduce((acc, e) => { acc[e.domain] = e.name return acc }, {}) const files = [{ name: paths.faviconsManifest, source: JSON.stringify(faviconsManifest), }, ...favicons] return file(files) .pipe(dest(paths.favicons)) })) task("docs-full", series("favicons", "docs")) 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 app = express() const handler = (allowedOrigin) => 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", "Access-Control-Allow-Origin": allowedOrigin, }) res.end(await fs.readFile(path.join("build", paths.scriptOut))) } app.get("/", handler("chrome-extension://mffcegbjcdejldmihkogmcnkgbbhioid")) app.get("/chrome", handler("chrome-extension://mffcegbjcdejldmihkogmcnkgbbhioid")) app.get("/firefox", handler("moz-extension://a7b04efeb-0b36-47f6-9f57-70293e5ee7b2")) app.listen(servePort) console.log(`web server is listening on port ${servePort}`) // eslint-disable-line no-console app.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"))