2017-12-02 02:48:58 +00:00
|
|
|
const gulp = require("gulp")
|
2019-06-02 01:55:21 +00:00
|
|
|
|
|
|
|
const { task, src, dest } = gulp
|
|
|
|
const { parallel, series } = gulp
|
|
|
|
|
2018-08-13 02:18:52 +00:00
|
|
|
const parcel = require("gulp-parcel")
|
2017-12-02 02:48:58 +00:00
|
|
|
const replace = require("gulp-replace")
|
|
|
|
const rename = require("gulp-rename")
|
|
|
|
const eslint = require("gulp-eslint")
|
2019-03-10 03:28:48 +00:00
|
|
|
const file = require("gulp-file")
|
2017-12-02 02:48:58 +00:00
|
|
|
const path = require("path")
|
|
|
|
const del = require("del")
|
2019-06-02 01:55:21 +00:00
|
|
|
const platforms = require("platform-folders")
|
2019-03-10 03:28:48 +00:00
|
|
|
const fs = require("fs").promises
|
|
|
|
const fetch = require("node-fetch")
|
2019-06-02 01:55:21 +00:00
|
|
|
const http = require("http")
|
2019-09-11 11:50:18 +00:00
|
|
|
const gulpIf = require("gulp-if")
|
2019-06-02 01:55:21 +00:00
|
|
|
|
|
|
|
const { COPYFILE_EXCL } = require("fs").constants
|
2017-12-02 02:48:58 +00:00
|
|
|
const { URL } = require("url")
|
2017-08-28 03:24:24 +00:00
|
|
|
|
2020-04-26 08:42:39 +00:00
|
|
|
const pkg = require("./package.json")
|
|
|
|
|
|
|
|
const copyrightYearOne = 2017
|
|
|
|
|
2020-04-26 06:57:09 +00:00
|
|
|
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(),
|
|
|
|
}
|
|
|
|
|
2019-05-24 00:33:48 +00:00
|
|
|
let srcFilesLoaded = false
|
|
|
|
let compl
|
|
|
|
let conf
|
|
|
|
let keys
|
|
|
|
let util
|
|
|
|
|
|
|
|
const requireSrcFiles = () => {
|
|
|
|
if (srcFilesLoaded) {
|
|
|
|
return
|
|
|
|
}
|
2020-04-26 06:57:09 +00:00
|
|
|
/* 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 */
|
2019-05-24 00:33:48 +00:00
|
|
|
srcFilesLoaded = true
|
|
|
|
}
|
2018-11-10 01:47:39 +00:00
|
|
|
|
2020-04-26 06:57:09 +00:00
|
|
|
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 = {}
|
|
|
|
}
|
2017-12-02 02:48:58 +00:00
|
|
|
}
|
2017-08-28 03:24:24 +00:00
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
const servePort = 9919
|
|
|
|
|
2017-10-31 07:27:41 +00:00
|
|
|
// This notice will be injected into the generated README.md file
|
2017-10-31 05:39:21 +00:00
|
|
|
const disclaimer = `\
|
|
|
|
<!--
|
|
|
|
|
|
|
|
NOTICE:
|
|
|
|
This is an automatically generated file - Do not edit it directly.
|
2019-06-02 01:55:21 +00:00
|
|
|
The source file is ${paths.readme}
|
2017-10-31 05:39:21 +00:00
|
|
|
|
2017-12-02 02:48:58 +00:00
|
|
|
-->`
|
2017-10-31 05:39:21 +00:00
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
task("clean", () => del(["build", ".cache", ".tmp-gulp-compile-*"]))
|
2019-03-14 17:42:52 +00:00
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
task("clean-favicons", () => del([paths.favicons]))
|
2017-08-28 03:24:24 +00:00
|
|
|
|
2019-09-11 11:50:18 +00:00
|
|
|
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),
|
|
|
|
)))
|
2019-03-07 22:13:49 +00:00
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
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()
|
|
|
|
})
|
2019-03-07 22:13:49 +00:00
|
|
|
|
2020-04-26 08:42:39 +00:00
|
|
|
const parseContributor = (contributor) => {
|
|
|
|
let c = contributor
|
|
|
|
if (typeof contributor === "string") {
|
|
|
|
const m = contributor.match(/^(?<name>.*?)\s*(<(?<email>.*?)>)?\s*(\((?<url>.*?)\))?$/)
|
|
|
|
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 ? `<a href="${c.url}">` : ""}${c.name}${c.url ? "</a>" : ""}`
|
|
|
|
}
|
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
task("docs", parallel(async () => {
|
2019-05-24 00:33:48 +00:00
|
|
|
requireSrcFiles()
|
2020-04-26 06:57:09 +00:00
|
|
|
await loadFaviconsManifest()
|
2019-05-24 00:33:48 +00:00
|
|
|
|
2017-12-02 02:48:58 +00:00
|
|
|
const screens = {}
|
|
|
|
let screenshotList = ""
|
2019-03-10 03:28:48 +00:00
|
|
|
|
|
|
|
const screenshots = await fs.readdir(path.join(__dirname, paths.screenshots))
|
|
|
|
screenshots.forEach((s) => {
|
|
|
|
const name = path.basename(s, ".png").split("-")
|
|
|
|
const alias = name[0]
|
2017-12-02 02:48:58 +00:00
|
|
|
if (!screens[alias]) {
|
|
|
|
screens[alias] = []
|
|
|
|
}
|
|
|
|
screens[alias].push(path.join(paths.screenshots, path.basename(s)))
|
|
|
|
})
|
2018-11-10 01:47:39 +00:00
|
|
|
|
2019-03-10 03:28:48 +00:00
|
|
|
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
|
2017-12-02 02:48:58 +00:00
|
|
|
if (a < b) return -1
|
|
|
|
if (a > b) return 1
|
|
|
|
return 0
|
2019-03-10 03:28:48 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
complTable = await complTable.reduce(async (acc1p, k) => {
|
|
|
|
const acc1 = await acc1p
|
2017-12-02 02:48:58 +00:00
|
|
|
const c = compl[k]
|
2019-03-10 03:28:48 +00:00
|
|
|
const u = new URL(c.domain ? `https://${c.domain}` : c.search)
|
|
|
|
const domain = u.hostname
|
2017-12-02 02:48:58 +00:00
|
|
|
let s = ""
|
|
|
|
if (screens[c.alias]) {
|
|
|
|
screens[c.alias].forEach((url, i) => {
|
|
|
|
const num = (i > 0) ? ` ${i + 1}` : ""
|
2019-03-10 03:28:48 +00:00
|
|
|
s += `<a href="#${c.name}${num.replace(" ", "-")}">:framed_picture:</a>`
|
2017-12-02 02:48:58 +00:00
|
|
|
screenshotList += `##### ${c.name}${num}\n`
|
|
|
|
screenshotList += `![${c.name} screenshot](./${url})\n\n`
|
|
|
|
})
|
|
|
|
}
|
2020-04-26 06:57:09 +00:00
|
|
|
|
|
|
|
const favicon = faviconsManifest[domain] ? `<img src="./assets/favicons/${faviconsManifest[domain]}" width="16px"> ` : ""
|
|
|
|
|
2019-03-10 03:28:48 +00:00
|
|
|
return `${acc1}
|
|
|
|
<tr>
|
|
|
|
<td><a href="${u.protocol}//${domain}">${favicon}</a></td>
|
|
|
|
<td><code>${c.alias}</code></td>
|
|
|
|
<td>${c.name}</td>
|
|
|
|
<td><a href="${u.protocol}//${domain}">${domain}</a></td>
|
|
|
|
<td>${s}</td>
|
|
|
|
</tr>`
|
|
|
|
}, Promise.resolve(""))
|
|
|
|
|
|
|
|
keysTable = await keysTable.reduce(async (acc1p, domain) => {
|
|
|
|
const acc1 = await acc1p
|
2018-11-10 01:47:39 +00:00
|
|
|
const header = "<tr><td><strong>Mapping</strong></td><td><strong>Description</strong></td></tr>"
|
|
|
|
const c = keys.maps[domain]
|
|
|
|
const maps = c.reduce((acc2, map) => {
|
2018-11-16 09:04:51 +00:00
|
|
|
let leader = ""
|
|
|
|
if (typeof map.leader !== "undefined") {
|
|
|
|
leader = map.leader // eslint-disable-line prefer-destructuring
|
|
|
|
} else if (domain === "global") {
|
|
|
|
leader = ""
|
|
|
|
} else {
|
|
|
|
leader = conf.siteleader
|
|
|
|
}
|
2018-11-10 01:47:39 +00:00
|
|
|
const mapStr = util.escape(`${leader}${map.alias}`.replace(" ", "<space>"))
|
|
|
|
return `${acc2}<tr><td><code>${mapStr}</code></td><td>${map.description}</td></tr>\n`
|
|
|
|
}, "")
|
2019-03-10 03:28:48 +00:00
|
|
|
let domainStr = "<strong>global</strong>"
|
2020-04-26 06:57:09 +00:00
|
|
|
let favicon = ""
|
2019-03-10 03:28:48 +00:00
|
|
|
if (domain !== "global") {
|
2020-04-26 06:57:09 +00:00
|
|
|
favicon = faviconsManifest[domain] ? `<img src="./assets/favicons/${faviconsManifest[domain]}" width="16px"> ` : ""
|
2019-03-10 03:28:48 +00:00
|
|
|
domainStr = `<a href="//${domain}">${favicon}${domain}</a>`
|
|
|
|
}
|
2018-11-10 01:47:39 +00:00
|
|
|
return `${acc1}<tr><th colspan="2">${domainStr}</th></tr>${header}\n${maps}`
|
2019-03-10 03:28:48 +00:00
|
|
|
}, Promise.resolve(""))
|
2018-11-10 01:47:39 +00:00
|
|
|
|
2020-04-26 08:42:39 +00:00
|
|
|
const year = (new Date()).getFullYear()
|
|
|
|
const copyrightYears = `${copyrightYearOne !== year ? `${copyrightYearOne}-${year}` : copyrightYearOne}`
|
|
|
|
let copyright = `<p><h4>Author</h4>© ${copyrightYears} ${parseContributor(pkg.author)}</p>`
|
|
|
|
if (Array.isArray(pkg.contributors) && pkg.contributors.length > 0) {
|
|
|
|
copyright += "<p><h4>Contributors</h4><ul>"
|
|
|
|
copyright += pkg.contributors.reduce((acc, c) => `${acc}<li>${parseContributor(c)}</li>`, "")
|
|
|
|
copyright += "</ul></p>"
|
|
|
|
}
|
|
|
|
copyright += `<p><h4>License</h4>Released under the <a href="./LICENSE">${pkg.license} License</a></p>`
|
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
return src([paths.readme])
|
2017-12-02 03:10:34 +00:00
|
|
|
.pipe(replace("<!--{{DISCLAIMER}}-->", disclaimer))
|
|
|
|
.pipe(replace("<!--{{COMPL_COUNT}}-->", Object.keys(compl).length))
|
2018-11-10 01:47:39 +00:00
|
|
|
.pipe(replace("<!--{{COMPL_TABLE}}-->", complTable))
|
|
|
|
.pipe(replace("<!--{{KEYS_MAPS_COUNT}}-->", Object.keys(keys.maps).reduce((acc, m) => acc + m.length, 0)))
|
|
|
|
.pipe(replace("<!--{{KEYS_SITES_COUNT}}-->", Object.keys(keys.maps).length))
|
|
|
|
.pipe(replace("<!--{{KEYS_TABLE}}-->", keysTable))
|
2017-12-02 03:10:34 +00:00
|
|
|
.pipe(replace("<!--{{SCREENSHOTS}}-->", screenshotList))
|
2020-04-26 08:42:39 +00:00
|
|
|
.pipe(replace("<!--{{COPYRIGHT}}-->", copyright))
|
2019-06-02 01:55:21 +00:00
|
|
|
.pipe(rename(paths.readmeOut))
|
|
|
|
.pipe(dest("."))
|
2019-03-10 03:28:48 +00:00
|
|
|
}))
|
|
|
|
|
|
|
|
const getFavicon = async ({ domain, favicon }, timeout = 5000) => {
|
|
|
|
const url = favicon
|
|
|
|
let data
|
2020-04-26 06:57:09 +00:00
|
|
|
let ext = path.extname(new URL(favicon).pathname)
|
2019-03-10 03:28:48 +00:00
|
|
|
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) {
|
2019-03-10 17:36:27 +00:00
|
|
|
process.stdout.write(`no favicon found for ${url}: ${e}\n`)
|
2020-04-26 06:57:09 +00:00
|
|
|
ext = ".ico"
|
2019-03-10 03:28:48 +00:00
|
|
|
// transparent pixel
|
|
|
|
data = Buffer.from(
|
|
|
|
"AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA==",
|
2020-04-26 06:57:09 +00:00
|
|
|
"base64",
|
2019-03-10 03:28:48 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
return {
|
2020-04-26 06:57:09 +00:00
|
|
|
domain,
|
2019-03-10 03:28:48 +00:00
|
|
|
name: `${domain}${ext}`,
|
|
|
|
source: data,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
task("favicons", series("clean-favicons", async () => {
|
2019-05-24 00:33:48 +00:00
|
|
|
requireSrcFiles()
|
|
|
|
|
2019-03-10 03:28:48 +00:00
|
|
|
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)
|
2019-09-11 11:51:27 +00:00
|
|
|
.filter((k) => k !== "global")
|
|
|
|
.map((k) => ({
|
2019-03-10 03:28:48 +00:00
|
|
|
domain: k,
|
|
|
|
favicon: `${new URL(`https://${k}`).origin}/favicon.ico`,
|
|
|
|
})),
|
2019-06-02 01:55:21 +00:00
|
|
|
).filter((e, i, arr) => i === arr.indexOf(e)) // Keep only first occurrence of each element
|
2019-03-10 03:28:48 +00:00
|
|
|
|
2019-09-11 11:51:27 +00:00
|
|
|
const favicons = (await Promise.all(sites.map(async (site) => getFavicon(site))))
|
|
|
|
.filter((e) => e !== undefined)
|
2020-04-26 06:57:09 +00:00
|
|
|
|
|
|
|
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)
|
2019-06-02 01:55:21 +00:00
|
|
|
.pipe(dest(paths.favicons))
|
2019-03-14 17:42:52 +00:00
|
|
|
}))
|
2017-10-31 05:39:21 +00:00
|
|
|
|
2020-04-26 06:57:09 +00:00
|
|
|
task("docs-full", series("favicons", "docs"))
|
2019-03-10 03:28:48 +00:00
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
task("build",
|
2019-03-07 22:13:49 +00:00
|
|
|
series(
|
2019-06-02 01:55:21 +00:00
|
|
|
parallel(
|
|
|
|
"check-priv",
|
|
|
|
"clean",
|
|
|
|
),
|
|
|
|
parallel(
|
|
|
|
"lint",
|
|
|
|
() => src(paths.entry, { read: false })
|
|
|
|
.pipe(parcel())
|
2019-06-03 05:25:50 +00:00
|
|
|
.pipe(rename(paths.scriptOut))
|
2019-06-02 01:55:21 +00:00
|
|
|
.pipe(dest("build")),
|
2020-04-26 06:57:09 +00:00
|
|
|
),
|
2019-03-07 22:13:49 +00:00
|
|
|
))
|
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
task("dist", parallel("docs-full", "build"))
|
2019-03-10 17:31:30 +00:00
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
task("install", series("build", () => src(path.join("build", paths.scriptOut))
|
|
|
|
.pipe(dest(paths.installDir))))
|
2019-03-07 22:13:49 +00:00
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
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",
|
|
|
|
})
|
2019-06-03 05:25:50 +00:00
|
|
|
res.end(await fs.readFile(path.join("build", paths.scriptOut)))
|
2019-06-02 01:55:21 +00:00
|
|
|
})
|
|
|
|
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)
|
2019-03-07 22:13:49 +00:00
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
task("serve-build", parallel("watch-build", "serve-simple"))
|
2019-03-07 22:13:49 +00:00
|
|
|
|
2019-06-02 01:55:21 +00:00
|
|
|
task("serve", series("serve-build"))
|
|
|
|
task("watch", series("watch-install"))
|
|
|
|
task("default", series("build"))
|