/** * Converts the given string to an HTML element. * * @param string the string to convert to an HTML element * @param query the type of element to return * @returns {HTMLElement} the HTML element described by the given string */ const stringToHtml = function(string, query) { return (new DOMParser()).parseFromString(string, "text/html").body.querySelector(query); } /** * Alias for `document.querySelector`. * * @param q {string} the query string * @returns {HTMLElement} the element identified by the query string */ const $ = q => document.querySelector(q); /** * Runs the given function once the page is loaded. * * This function can be used multiple times. It does not overwrite existing callbacks for the page load event. If the * page has already loaded when this function is invoked, `fun` is invoked immediately inside this function. * * @param fun {function(...*): *} the function to run */ const doAfterLoad = function(fun) { if (document.readyState === "complete") { fun(); return; } const oldOnLoad = onload || (() => { }); onload = (() => { oldOnLoad(); fun(); }); }; /** * Creates a navigation element for navigating through the website. * * Fetches entries asynchronously from the website's API. * * @param [highlightPath] {String} the path to highlight together with its parents * @param [cb] {Function} the callback to execute on the fetched entries, to prevent the need to re-fetch elsewhere * @returns {HTMLElement} a base navigation element that will eventually be filled with contents */ const nav = function(highlightPath = "", cb = undefined) { const base = stringToHtml( ``, "ul" ); fetch("https://fwdekker.com/api/nav/") .then(it => it.json()) .then(json => { if (cb !== undefined) cb(json); json.entries.forEach(entry => base.appendChild(stringToHtml(unpackEntry(entry, "/", highlightPath), "li"))); }) .catch(e => { console.error("Failed to fetch navigation elements", e); return []; }); const nav = stringToHtml( ``, "nav" ); nav.appendChild(base); return nav; }; /** * Unpacks a navigation entry returned from the navigation API into an HTML element. * * @param entry {Object} the entry to unpack * @param [path] {number} the current path traversed, found by joining the names of the entries with `/`s; always starts * and ends with a `/` * @param [highlightPath] {String} the path to highlight together with its parents * @returns {string} the navigation list entry as HTML, described by its children */ const unpackEntry = function(entry, path = "/", highlightPath = "") { const shouldHighlight = highlightPath.startsWith(`${path + entry.name}/`); const isExternalLink = !(/^https:\/\/.*fwdekker.com/.test(entry.link)) && entry.link !== "#"; const formattedName = (isExternalLink ? "⎋ " : "") + entry.name; if (entry.entries.length === 0) return "" + `
  • ${formattedName}
  • `; const depth = path.split("/").length - 2; // -1 because count parts, then another -1 because of leading `/` const arrow = depth === 0 ? "▾" : "▸"; return "" + `
  • ` + `${formattedName} ${arrow}` + `` + `
  • `; }; /** * Creates a header element with the given title and description. * * @param [title] {string} the title to display, possibly including HTML * @param [description] {string} the description to display, possibly including HTML * @returns {HTMLElement} a header element */ const header = function({title, description}) { if (title === undefined && description === undefined) return stringToHtml(`
    `, "header"); return stringToHtml( `
    ` + (title !== undefined ? `

    ${title}

    ` : "") + (description !== undefined ? `

    ${description}

    ` : "") + `
    `, "header" ); }; /** * Creates a footer element with the given data. * * Setting an argument to `undefined` or not giving that argument will cause the default value to be used. Setting an * argument to `null` will result in a footer without the corresponding element. * * @param [author] {string|null|undefined} the author * @param [authorURL] {string|null|undefined} the URL to link the author's name to * @param [license] {string|null|undefined} the type of license * @param [licenseURL] {string|null|undefined} the URL to the license file * @param [vcs] {string|null|undefined} the type of version control * @param [vcsURL] {string|null|undefined} the URL to the repository * @param [version] {string|null|undefined} the page version * @param [privacyPolicyURL] {string|null|undefined} the URL to the privacy policy * @returns {HTMLElement} a footer element */ const footer = function( { author = undefined, authorURL = undefined, license = undefined, licenseURL = undefined, vcs = undefined, vcsURL = undefined, version = undefined, privacyPolicyURL = undefined }) { if (author === undefined) author = "F.W. Dekker"; if (authorURL === undefined) authorURL = "https://fwdekker.com/"; if (license === undefined) license = "MIT License"; if (licenseURL === undefined && vcsURL !== undefined) licenseURL = `${vcsURL}src/branch/master/LICENSE`; if (vcs === undefined && vcsURL !== undefined) vcs = "git"; if (privacyPolicyURL === undefined) privacyPolicyURL = "https://fwdekker.com/privacy/"; return stringToHtml( ``, "footer"); }; /** * Constructs a link that is used in footers. * * @param prefix {string} the text to display before the text if the text is not undefined * @param text {string|null} the text to display, or `null` if the returned element should be empty * @param url {string|null} the URL to link the text to, or `null` if the text should not be a link * @param suffix {string} the text to display after the text if the text is not undefined * @returns {string} a footer link element */ const footerLink = function(prefix, text, url, suffix) { if (text === null) return ""; return `${prefix}${url !== null ? `${text}` : text}${suffix}`; }; /** * Runs the functions `nav`, `header`, and `footer` after the page has loaded using properties defined in the meta tags. * * The remaining functions are invoked only if the corresponding `target` meta property is set to select an existing * element. The HTML element returned by the function is then added as a child to the element specified by the query * selector in the `target` property. The parameters to the function invocation can be set using other meta properties. * The format of meta properties is `fwd::`, where `property` is the same as the name of the * parameter of that function, except that instead of camelcase words are separated by dashes. For example, `vcsURL` * becomes `vcs-url`. * * Meta properties can be set by including `` in the HTML * page on which this module is included. The `` is then passed without modification as a parameter to the * function. Leaving out the `` by writing `` instead results in passing * `null` as the value. Not including the meta tag at all corresponds to passing `undefined` to the function. See the * documentation of the respective functions for more details on the parameters that they accept. * * Note that the function is invoked only if `fwd::target` is a query selector for an existing element. This * means that it is possible to mix how the functions are invoked; for example, one can use meta properties to pass * parameters to `nav`, but also invoke `header` in a separate function manually. */ doAfterLoad(() => { const getMetaProperty = (name) => { const element = $(`meta[name="${name}"]`); return element === null ? undefined : element.getAttribute("content"); }; const navTarget = $(getMetaProperty("fwd:nav:target")); if (navTarget !== null) { navTarget.appendChild(nav(getMetaProperty("fwd:nav:highlight-path"))); } const headerTarget = $(getMetaProperty("fwd:header:target")); if (headerTarget !== null) { headerTarget.appendChild(header({ title: getMetaProperty("fwd:header:title"), description: getMetaProperty("fwd:header:description"), })); } const footerTarget = $(getMetaProperty("fwd:footer:target")); if (footerTarget !== null) { footerTarget.appendChild(footer({ author: getMetaProperty("fwd:footer:author"), authorURL: getMetaProperty("fwd:footer:author-url"), license: getMetaProperty("fwd:footer:license"), licenseURL: getMetaProperty("fwd:footer:license-url"), vcs: getMetaProperty("fwd:footer:vcs"), vcsURL: getMetaProperty("fwd:footer:vcs-url"), version: getMetaProperty("fwd:footer:version"), privacyPolicyURL: getMetaProperty("fwd:footer:privacy-policy-url"), })); } }); // Export to namespace fwdekker = {stringToHtml, $, doAfterLoad, nav, header, footer};