refactor: replace gulp by webpack and npm scripts (#258)

BREAKING CHANGE: We have replaced `gulp` with `webpack` and `npm scripts` to build this theme. If you build it on your own or use build commands during the deployment, you may have to adjust your setup.

BREAKING CHANGE: The `GeekblogIcons` font is using the icon name as Unicode now. As a consequence, you have to replace all references to Icons from this font if you have customized the theme.

BREAKING CHANGE: We have refactored the search integration to split Hugo templates from JavaScript code. To get it working again, you need to adjust the `outputFormats` and `outputs` in your Hugo configuration file, as [documented](https://geekdocs.de/usage/configuration/#site-configuration).
This commit is contained in:
Robert Kaussow 2022-01-06 13:58:10 +01:00 committed by GitHub
parent 2ac2a9faab
commit 5c5e2d59cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 18705 additions and 5208 deletions

View file

@ -1,21 +1,31 @@
import { applyTheme } from "./darkmode"
import { createCopyButton } from "./copycode.js"
import Clipboard from "clipboard"
;(() => {
applyTheme()
})()
document.addEventListener("DOMContentLoaded", function (event) {
var clipboard = new ClipboardJS(".clip");
let clipboard = new Clipboard(".clip")
clipboard.on("success", function (e) {
const trigger = e.trigger;
const trigger = e.trigger
if (trigger.hasAttribute("data-copy-feedback")) {
trigger.classList.add("gdoc-post__codecopy--success");
trigger.querySelector(".icon.copy").classList.add("hidden");
trigger.querySelector(".icon.check").classList.remove("hidden");
trigger.classList.add("gdoc-post__codecopy--success")
trigger.querySelector(".icon.copy").classList.add("hidden")
trigger.querySelector(".icon.check").classList.remove("hidden")
setTimeout(function () {
trigger.classList.remove("gdoc-post__codecopy--success");
trigger.querySelector(".icon.copy").classList.remove("hidden");
trigger.querySelector(".icon.check").classList.add("hidden");
}, 3000);
trigger.classList.remove("gdoc-post__codecopy--success")
trigger.querySelector(".icon.copy").classList.remove("hidden")
trigger.querySelector(".icon.check").classList.add("hidden")
}, 3000)
}
e.clearSelection();
});
});
e.clearSelection()
})
document.querySelectorAll(".highlight").forEach((highlightDiv) => createCopyButton(highlightDiv))
})

5
src/js/config.js Normal file
View file

@ -0,0 +1,5 @@
export const DARK_MODE = "dark"
export const LIGHT_MODE = "light"
export const AUTO_MODE = "auto"
export const THEME = "hugo-geekdoc"
export const TOGGLE_MODES = [AUTO_MODE, DARK_MODE, LIGHT_MODE]

View file

@ -1,34 +1,23 @@
function createCopyButton(highlightDiv) {
const button = document.createElement("span");
export function createCopyButton(highlightDiv) {
const button = document.createElement("span")
let selector = "pre > code"
if (highlightDiv.querySelector(".lntable")) {
selector = ".lntable .lntd:last-child pre > code";
} else {
selector = "pre > code";
selector = ".lntable .lntd:last-child pre > code"
}
const codeToCopy = highlightDiv.querySelector(selector).innerText.trim();
const codeToCopy = highlightDiv.querySelector(selector).innerText.trim()
button.classList.add(
"flex",
"align-center",
"justify-center",
"clip",
"gdoc-post__codecopy"
);
button.type = "button";
button.classList.add("flex", "align-center", "justify-center", "clip", "gdoc-post__codecopy")
button.type = "button"
button.innerHTML =
'<svg class="icon copy"><use xlink:href="#gdoc_copy"></use></svg>' +
'<svg class="icon check hidden"><use xlink:href="#gdoc_check"></use></svg>';
button.setAttribute("data-clipboard-text", codeToCopy);
button.setAttribute("data-copy-feedback", "Copied!");
button.setAttribute("role", "button");
button.setAttribute("aria-label", "Copy");
'<svg class="icon check hidden"><use xlink:href="#gdoc_check"></use></svg>'
button.setAttribute("data-clipboard-text", codeToCopy)
button.setAttribute("data-copy-feedback", "Copied!")
button.setAttribute("role", "button")
button.setAttribute("aria-label", "Copy")
highlightDiv.classList.add("gdoc-post__codecontainer");
highlightDiv.insertBefore(button, highlightDiv.firstChild);
highlightDiv.classList.add("gdoc-post__codecontainer")
highlightDiv.insertBefore(button, highlightDiv.firstChild)
}
document
.querySelectorAll(".highlight")
.forEach((highlightDiv) => createCopyButton(highlightDiv));

View file

@ -1,51 +1,53 @@
const DARK_MODE = "dark";
const LIGHT_MODE = "light";
const AUTO_MODE = "auto";
const THEME = "hugo-geekdoc";
import Storage from "store2"
const TOGGLE_MODES = [AUTO_MODE, DARK_MODE, LIGHT_MODE];
import { TOGGLE_MODES, THEME, AUTO_MODE } from "./config.js"
(applyTheme = function (init = true) {
let html = document.documentElement;
let currentMode = TOGGLE_MODES.includes(localStorage.getItem(THEME))
? localStorage.getItem(THEME)
: AUTO_MODE;
document.addEventListener("DOMContentLoaded", (event) => {
const darkModeToggle = document.getElementById("gdoc-dark-mode")
html.setAttribute("class", "color-toggle-" + currentMode);
localStorage.setItem(THEME, currentMode);
darkModeToggle.onclick = function () {
let lstore = Storage.namespace(THEME)
let currentMode = lstore.get("color-mode")
let nextMode = toggle(TOGGLE_MODES, currentMode)
lstore.set("color-mode", TOGGLE_MODES[nextMode])
applyTheme(false)
}
})
export function applyTheme(init = true) {
if (Storage.isFake()) return
let lstore = Storage.namespace(THEME)
let html = document.documentElement
let currentMode = TOGGLE_MODES.includes(lstore.get("color-mode"))
? lstore.get("color-mode")
: AUTO_MODE
html.setAttribute("class", "color-toggle-" + currentMode)
lstore.set("color-mode", currentMode)
if (currentMode === AUTO_MODE) {
html.removeAttribute("color-mode");
html.removeAttribute("color-mode")
} else {
html.setAttribute("color-mode", currentMode);
html.setAttribute("color-mode", currentMode)
}
if (!init) {
// Reload required to re-initialise e.g. Mermaid with the new theme and re-parse the Mermaid code blocks.
location.reload();
// Reload required to re-initialise e.g. Mermaid with the new theme
// and re-parse the Mermaid code blocks.
location.reload()
}
})();
document.addEventListener("DOMContentLoaded", (event) => {
const darkModeToggle = document.getElementById("gdoc-dark-mode");
darkModeToggle.onclick = function () {
let currentMode = localStorage.getItem(THEME);
let nextMode = toggle(TOGGLE_MODES, currentMode);
localStorage.setItem(THEME, TOGGLE_MODES[nextMode]);
applyTheme(false);
};
});
}
function toggle(list = [], value) {
current = list.indexOf(value);
max = list.length - 1;
next = 0;
let current = list.indexOf(value)
let max = list.length - 1
let next = 0
if (current < max) {
next = current + 1;
next = current + 1
}
return next;
return next
}

View file

@ -7,30 +7,25 @@
* strings for iteratees.
*/
const groupBy = (e, ...t) => {
export const groupBy = (e, ...t) => {
let r = e.map((e) => t.map((t) => t(e))),
a = {};
a = {}
return (
r.forEach((t, r) => {
let l = (_simpleAt(a, t) || []).concat([e[r]]);
_simpleSet(a, t, l);
let l = (_simpleAt(a, t) || []).concat([e[r]])
_simpleSet(a, t, l)
}),
a
);
)
},
_isPlainObject = (e) =>
null != e && "object" == typeof e && e.constructor == Object,
_isPlainObject = (e) => null != e && "object" == typeof e && e.constructor == Object,
_parsePath = (e) => (Array.isArray(e) ? e : `${e}`.split(".")),
_simpleAt = (e, t) =>
_parsePath(t).reduce(
(e, t) => (null != e && e.hasOwnProperty(t) ? e[t] : void 0),
e
),
_parsePath(t).reduce((e, t) => (null != e && e.hasOwnProperty(t) ? e[t] : void 0), e),
_simpleSet = (e, t, r) =>
_parsePath(t).reduce((e, t, a, l) => {
let s = a === l.length - 1;
let s = a === l.length - 1
return (
(e.hasOwnProperty(t) && (s || _isPlainObject(e[t]))) || (e[t] = {}),
s ? (e[t] = r) : e[t]
);
}, e);
(e.hasOwnProperty(t) && (s || _isPlainObject(e[t]))) || (e[t] = {}), s ? (e[t] = r) : e[t]
)
}, e)

View file

@ -1,3 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
renderMathInElement(document.body);
});

9
src/js/katex.js Normal file
View file

@ -0,0 +1,9 @@
import "katex/dist/katex.css"
document.addEventListener("DOMContentLoaded", function () {
import("katex/dist/contrib/auto-render")
.then(({ default: renderMathInElement }) => {
renderMathInElement(document.body)
})
.catch((error) => console.error(error))
})

View file

@ -1,23 +0,0 @@
document.addEventListener("DOMContentLoaded", function (event) {
let currentMode = localStorage.getItem(THEME);
let darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
let primaryColor = "#ececff";
let darkMode = false;
if (
currentMode === DARK_MODE ||
(currentMode === AUTO_MODE && darkModeQuery.matches)
) {
primaryColor = "#6C617E";
darkMode = true;
}
mermaid.initialize({
flowchart: { useMaxWidth: true },
theme: "base",
themeVariables: {
darkMode: darkMode,
primaryColor: primaryColor,
},
});
});

29
src/js/mermaid.js Normal file
View file

@ -0,0 +1,29 @@
import Storage from "store2"
import { DARK_MODE, THEME, AUTO_MODE } from "./config.js"
document.addEventListener("DOMContentLoaded", function (event) {
let lstore = Storage.namespace(THEME)
let currentMode = lstore.get("color-mode")
let darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)")
let primaryColor = "#ececff"
let darkMode = false
if (currentMode === DARK_MODE || (currentMode === AUTO_MODE && darkModeQuery.matches)) {
primaryColor = "#6C617E"
darkMode = true
}
import("mermaid")
.then(({ default: md }) => {
md.initialize({
flowchart: { useMaxWidth: true },
theme: "base",
themeVariables: {
darkMode: darkMode,
primaryColor: primaryColor
}
})
})
.catch((error) => console.error(error))
})

200
src/js/search.js Normal file
View file

@ -0,0 +1,200 @@
const { groupBy } = require("./groupBy")
const FlexSearch = require("flexsearch")
const Ajv = require("ajv")
document.addEventListener("DOMContentLoaded", function (event) {
const ajv = new Ajv()
const input = document.querySelector("#gdoc-search-input")
const results = document.querySelector("#gdoc-search-results")
const configSchema = {
type: "object",
properties: {
dataFile: {
type: "string"
},
indexConfig: {
type: ["object", "null"]
},
showParent: {
type: "boolean"
}
},
additionalProperties: false
}
getJson("/searchconfig.json", function (searchConfig) {
const configValidate = ajv.compile(configSchema)
const valid = configValidate(searchConfig)
if (!valid)
throw AggregateError(
configValidate.errors.map(
(err) =>
new Error(["Validation error:", err.instancePath, err.keyword, err.message].join(" "))
),
"Schema validation failed"
)
if (input) {
input.addEventListener("focus", () => {
init(input, searchConfig)
})
input.addEventListener("keyup", () => {
search(input, results, searchConfig)
})
}
})
})
function init(input, searchConfig) {
input.removeEventListener("focus", init)
const indexCfgDefaults = {
tokenize: "forward"
}
const indexCfg = searchConfig.indexConfig ? searchConfig.indexConfig : indexCfgDefaults
const dataUrl = searchConfig.dataFile
indexCfg.document = {
key: "id",
index: ["title", "content"],
store: ["title", "href", "parent"]
}
const index = new FlexSearch.Document(indexCfg)
window.geekdocSearchIndex = index
getJson(dataUrl, function (data) {
data.forEach((obj) => {
window.geekdocSearchIndex.add(obj)
})
})
}
function search(input, results, searchConfig) {
const searchCfg = {
enrich: true,
limit: 10
}
while (results.firstChild) {
results.removeChild(results.firstChild)
}
if (!input.value) {
return results.classList.remove("has-hits")
}
let searchHits = flattenHits(window.geekdocSearchIndex.search(input.value, searchCfg))
if (searchHits.length < 1) {
return results.classList.remove("has-hits")
}
results.classList.add("has-hits")
if (searchConfig.showParent === true) {
searchHits = groupBy(searchHits, (hit) => hit.parent)
}
const items = []
if (searchConfig.showParent === true) {
for (const section in searchHits) {
const item = document.createElement("li"),
title = item.appendChild(document.createElement("span")),
subList = item.appendChild(document.createElement("ul"))
title.textContent = section
createLinks(searchHits[section], subList)
items.push(item)
}
} else {
const item = document.createElement("li"),
title = item.appendChild(document.createElement("span")),
subList = item.appendChild(document.createElement("ul"))
title.textContent = "Results"
createLinks(searchHits, subList)
items.push(item)
}
items.forEach((item) => {
results.appendChild(item)
})
}
/**
* Creates links to given fields and either returns them in an array or attaches them to a target element
* @param {Object} fields Page to which the link should point to
* @param {HTMLElement} target Element to which the links should be attatched
* @returns {Array} If target is not specified, returns an array of built links
*/
function createLinks(pages, target) {
const items = []
for (const page of pages) {
const item = document.createElement("li"),
entry = item.appendChild(document.createElement("span")),
a = entry.appendChild(document.createElement("a"))
entry.classList.add("flex")
a.href = page.href
a.textContent = page.title
a.classList.add("gdoc-search__entry")
if (target) {
target.appendChild(item)
continue
}
items.push(item)
}
return items
}
function fetchErrors(response) {
if (!response.ok) {
throw Error("Failed to fetch '" + response.url + "': " + response.statusText)
}
return response
}
function getJson(src, callback) {
fetch(src)
.then(fetchErrors)
.then((response) => response.json())
.then((json) => callback(json))
.catch(function (error) {
if (error instanceof AggregateError) {
console.error(error.message)
error.errors.forEach((element) => {
console.error(element)
})
} else {
console.error(error)
}
})
}
function flattenHits(results) {
const items = []
const map = new Map()
for (const field of results) {
for (const page of field.result) {
if (!map.has(page.doc.href)) {
map.set(page.doc.href, true)
items.push(page.doc)
}
}
}
return items
}