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:
parent
2ac2a9faab
commit
5c5e2d59cb
122 changed files with 18705 additions and 5208 deletions
|
@ -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
5
src/js/config.js
Normal 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]
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
document.addEventListener("DOMContentLoaded", function () {
|
||||
renderMathInElement(document.body);
|
||||
});
|
9
src/js/katex.js
Normal file
9
src/js/katex.js
Normal 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))
|
||||
})
|
|
@ -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
29
src/js/mermaid.js
Normal 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
200
src/js/search.js
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue