diff --git a/LICENSES/0BSD.txt b/LICENSES/0BSD.txt new file mode 100644 index 0000000..7ae8865 --- /dev/null +++ b/LICENSES/0BSD.txt @@ -0,0 +1,10 @@ +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE +FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/bin/forester/App.ml b/bin/forester/App.ml index 69461e8..c4021bb 100644 --- a/bin/forester/App.ml +++ b/bin/forester/App.ml @@ -13,6 +13,29 @@ let on_load o = Jv.call o "onLoad" let katex = Jv.get Jv.global "katex" +type _katex_config = { + displayMode: Jv.t; + output: Jstr.t; + leqno: Jv.t; + fleqn: Jv.t; + throwOnError: Jv.t; + errorColor: Jstr.t; + minRuleThickness: Jv.t; + colorIsTextColor: Jv.t; + maxSize: Jv.t; + maxExpand: Jv.t; + strict: Jv.t; + trust: Jv.t; (* Boolean or function*) + globalGroup: Jv.t +} + +let trust_config = + Jv.obj + [| + "trust", + Jv.of_bool true + |] + let render o = Jv.call o "render" let () = @@ -30,6 +53,7 @@ let () = [| Jv.get (El.to_jv elt) "textContent"; El.to_jv elt; + trust_config; |] ) (Jstr.v ".math") @@ -38,6 +62,8 @@ let () = Jv.repr f |] +(* let () = ignore @@ Jv.call htmx "logAll" [||] *) + let () = ignore @@ Ev.listen @@ -47,6 +73,7 @@ let () = Ev.( if Keyboard.ctrl_key ev && Keyboard.key ev = Jstr.v "k" then begin + Console.log ["hello"]; prevent_default e; ignore @@ ajax diff --git a/bin/forester/theme/REUSE.toml b/bin/forester/theme/REUSE.toml index 3889366..02ddff0 100644 --- a/bin/forester/theme/REUSE.toml +++ b/bin/forester/theme/REUSE.toml @@ -21,3 +21,9 @@ path = ["*.xsl", "*.png", "*.css", "*.sh", "*.ico", "*.json", "**/**.js", "index precedence = "override" SPDX-FileCopyrightText = "2024 The Forester Project Contributors" SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = ["htmx.js"] +precedence = "override" +SPDX-FileCopyrightText = "2020, Big Sky Software" +SPDX-License-Identifier = "0BSD" diff --git a/bin/forester/theme/htmx.js b/bin/forester/theme/htmx.js new file mode 100644 index 0000000..6079063 --- /dev/null +++ b/bin/forester/theme/htmx.js @@ -0,0 +1,5829 @@ +var htmx = (function () { + "use strict"; + + // Public API + const htmx = { + // Tsc madness here, assigning the functions directly results in an invalid TypeScript output, but reassigning is fine + /* Event processing */ + /** @type {typeof onLoadHelper} */ + onLoad: null, + /** @type {typeof processNode} */ + process: null, + /** @type {typeof addEventListenerImpl} */ + on: null, + /** @type {typeof removeEventListenerImpl} */ + off: null, + /** @type {typeof triggerEvent} */ + trigger: null, + /** @type {typeof ajaxHelper} */ + ajax: null, + /* DOM querying helpers */ + /** @type {typeof find} */ + find: null, + /** @type {typeof findAll} */ + findAll: null, + /** @type {typeof closest} */ + closest: null, + /** + * Returns the input values that would resolve for a given element via the htmx value resolution mechanism + * + * @see https://htmx.org/api/#values + * + * @param {Element} elt the element to resolve values on + * @param {HttpVerb} type the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post** + * @returns {Object} + */ + values: function (elt, type) { + const inputValues = getInputValues(elt, type || "post"); + return inputValues.values; + }, + /* DOM manipulation helpers */ + /** @type {typeof removeElement} */ + remove: null, + /** @type {typeof addClassToElement} */ + addClass: null, + /** @type {typeof removeClassFromElement} */ + removeClass: null, + /** @type {typeof toggleClassOnElement} */ + toggleClass: null, + /** @type {typeof takeClassForElement} */ + takeClass: null, + /** @type {typeof swap} */ + swap: null, + /* Extension entrypoints */ + /** @type {typeof defineExtension} */ + defineExtension: null, + /** @type {typeof removeExtension} */ + removeExtension: null, + /* Debugging */ + /** @type {typeof logAll} */ + logAll: null, + /** @type {typeof logNone} */ + logNone: null, + /* Debugging */ + /** + * The logger htmx uses to log with + * + * @see https://htmx.org/api/#logger + */ + logger: null, + /** + * A property holding the configuration htmx uses at runtime. + * + * Note that using a [meta tag](https://htmx.org/docs/#config) is the preferred mechanism for setting these properties. + * + * @see https://htmx.org/api/#config + */ + config: { + /** + * Whether to use history. + * @type boolean + * @default true + */ + historyEnabled: true, + /** + * The number of pages to keep in **localStorage** for history support. + * @type number + * @default 10 + */ + historyCacheSize: 10, + /** + * @type boolean + * @default false + */ + refreshOnHistoryMiss: false, + /** + * The default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted. + * @type HtmxSwapStyle + * @default 'innerHTML' + */ + defaultSwapStyle: "innerHTML", + /** + * The default delay between receiving a response from the server and doing the swap. + * @type number + * @default 0 + */ + defaultSwapDelay: 0, + /** + * The default delay between completing the content swap and settling attributes. + * @type number + * @default 20 + */ + defaultSettleDelay: 20, + /** + * If true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present. + * @type boolean + * @default true + */ + includeIndicatorStyles: true, + /** + * The class to place on indicators when a request is in flight. + * @type string + * @default 'htmx-indicator' + */ + indicatorClass: "htmx-indicator", + /** + * The class to place on triggering elements when a request is in flight. + * @type string + * @default 'htmx-request' + */ + requestClass: "htmx-request", + /** + * The class to temporarily place on elements that htmx has added to the DOM. + * @type string + * @default 'htmx-added' + */ + addedClass: "htmx-added", + /** + * The class to place on target elements when htmx is in the settling phase. + * @type string + * @default 'htmx-settling' + */ + settlingClass: "htmx-settling", + /** + * The class to place on target elements when htmx is in the swapping phase. + * @type string + * @default 'htmx-swapping' + */ + swappingClass: "htmx-swapping", + /** + * Allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility. + * @type boolean + * @default true + */ + allowEval: true, + /** + * If set to false, disables the interpretation of script tags. + * @type boolean + * @default true + */ + allowScriptTags: true, + /** + * If set, the nonce will be added to inline scripts. + * @type string + * @default '' + */ + inlineScriptNonce: "", + /** + * If set, the nonce will be added to inline styles. + * @type string + * @default '' + */ + inlineStyleNonce: "", + /** + * The attributes to settle during the settling phase. + * @type string[] + * @default ['class', 'style', 'width', 'height'] + */ + attributesToSettle: ["class", "style", "width", "height"], + /** + * Allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates. + * @type boolean + * @default false + */ + withCredentials: false, + /** + * @type number + * @default 0 + */ + timeout: 0, + /** + * The default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later**. + * @type {'full-jitter' | ((retryCount:number) => number)} + * @default "full-jitter" + */ + wsReconnectDelay: "full-jitter", + /** + * The type of binary data being received over the WebSocket connection + * @type BinaryType + * @default 'blob' + */ + wsBinaryType: "blob", + /** + * @type string + * @default '[hx-disable], [data-hx-disable]' + */ + disableSelector: "[hx-disable], [data-hx-disable]", + /** + * @type {'auto' | 'instant' | 'smooth'} + * @default 'instant' + */ + scrollBehavior: "instant", + /** + * If the focused element should be scrolled into view. + * @type boolean + * @default false + */ + defaultFocusScroll: false, + /** + * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser + * @type boolean + * @default false + */ + getCacheBusterParam: false, + /** + * If set to true, htmx will use the View Transition API when swapping in new content. + * @type boolean + * @default false + */ + globalViewTransitions: false, + /** + * htmx will format requests with these methods by encoding their parameters in the URL, not the request body + * @type {(HttpVerb)[]} + * @default ['get', 'delete'] + */ + methodsThatUseUrlParams: ["get", "delete"], + /** + * If set to true, disables htmx-based requests to non-origin hosts. + * @type boolean + * @default false + */ + selfRequestsOnly: true, + /** + * If set to true htmx will not update the title of the document when a title tag is found in new content + * @type boolean + * @default false + */ + ignoreTitle: false, + /** + * Whether the target of a boosted element is scrolled into the viewport. + * @type boolean + * @default true + */ + scrollIntoViewOnBoost: true, + /** + * The cache to store evaluated trigger specifications into. + * You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * @type {Object|null} + * @default null + */ + triggerSpecsCache: null, + /** @type boolean */ + disableInheritance: false, + /** @type HtmxResponseHandlingConfig[] */ + responseHandling: [ + { code: "204", swap: false }, + { code: "[23]..", swap: true }, + { code: "[45]..", swap: false, error: true }, + ], + /** + * Whether to process OOB swaps on elements that are nested within the main response element. + * @type boolean + * @default true + */ + allowNestedOobSwaps: true, + }, + /** @type {typeof parseInterval} */ + parseInterval: null, + /** @type {typeof internalEval} */ + _: null, + version: "2.0.4", + }; + // Tsc madness part 2 + htmx.onLoad = onLoadHelper; + htmx.process = processNode; + htmx.on = addEventListenerImpl; + htmx.off = removeEventListenerImpl; + htmx.trigger = triggerEvent; + htmx.ajax = ajaxHelper; + htmx.find = find; + htmx.findAll = findAll; + htmx.closest = closest; + htmx.remove = removeElement; + htmx.addClass = addClassToElement; + htmx.removeClass = removeClassFromElement; + htmx.toggleClass = toggleClassOnElement; + htmx.takeClass = takeClassForElement; + htmx.swap = swap; + htmx.defineExtension = defineExtension; + htmx.removeExtension = removeExtension; + htmx.logAll = logAll; + htmx.logNone = logNone; + htmx.parseInterval = parseInterval; + htmx._ = internalEval; + + const internalAPI = { + addTriggerHandler, + bodyContains, + canAccessLocalStorage, + findThisElement, + filterValues, + swap, + hasAttribute, + getAttributeValue, + getClosestAttributeValue, + getClosestMatch, + getExpressionVars, + getHeaders, + getInputValues, + getInternalData, + getSwapSpecification, + getTriggerSpecs, + getTarget, + makeFragment, + mergeObjects, + makeSettleInfo, + oobSwap, + querySelectorExt, + settleImmediately, + shouldCancel, + triggerEvent, + triggerErrorEvent, + withExtensions, + }; + + const VERBS = ["get", "post", "put", "delete", "patch"]; + const VERB_SELECTOR = VERBS.map(function (verb) { + return "[hx-" + verb + "], [data-hx-" + verb + "]"; + }).join(", "); + + //= =================================================================== + // Utilities + //= =================================================================== + + /** + * Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes. + * + * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat** + * + * @see https://htmx.org/api/#parseInterval + * + * @param {string} str timing string + * @returns {number|undefined} + */ + function parseInterval(str) { + if (str == undefined) { + return undefined; + } + + let interval = NaN; + if (str.slice(-2) == "ms") { + interval = parseFloat(str.slice(0, -2)); + } else if (str.slice(-1) == "s") { + interval = parseFloat(str.slice(0, -1)) * 1000; + } else if (str.slice(-1) == "m") { + interval = parseFloat(str.slice(0, -1)) * 1000 * 60; + } else { + interval = parseFloat(str); + } + return isNaN(interval) ? undefined : interval; + } + + /** + * @param {Node} elt + * @param {string} name + * @returns {(string | null)} + */ + function getRawAttribute(elt, name) { + return elt instanceof Element && elt.getAttribute(name); + } + + /** + * @param {Element} elt + * @param {string} qualifiedName + * @returns {boolean} + */ + // resolve with both hx and data-hx prefixes + function hasAttribute(elt, qualifiedName) { + return ( + !!elt.hasAttribute && + (elt.hasAttribute(qualifiedName) || + elt.hasAttribute("data-" + qualifiedName)) + ); + } + + /** + * + * @param {Node} elt + * @param {string} qualifiedName + * @returns {(string | null)} + */ + function getAttributeValue(elt, qualifiedName) { + return ( + getRawAttribute(elt, qualifiedName) || + getRawAttribute(elt, "data-" + qualifiedName) + ); + } + + /** + * @param {Node} elt + * @returns {Node | null} + */ + function parentElt(elt) { + const parent = elt.parentElement; + if (!parent && elt.parentNode instanceof ShadowRoot) return elt.parentNode; + return parent; + } + + /** + * @returns {Document} + */ + function getDocument() { + return document; + } + + /** + * @param {Node} elt + * @param {boolean} global + * @returns {Node|Document} + */ + function getRootNode(elt, global) { + return elt.getRootNode + ? elt.getRootNode({ composed: global }) + : getDocument(); + } + + /** + * @param {Node} elt + * @param {(e:Node) => boolean} condition + * @returns {Node | null} + */ + function getClosestMatch(elt, condition) { + while (elt && !condition(elt)) { + elt = parentElt(elt); + } + + return elt || null; + } + + /** + * @param {Element} initialElement + * @param {Element} ancestor + * @param {string} attributeName + * @returns {string|null} + */ + function getAttributeValueWithDisinheritance( + initialElement, + ancestor, + attributeName, + ) { + const attributeValue = getAttributeValue(ancestor, attributeName); + const disinherit = getAttributeValue(ancestor, "hx-disinherit"); + var inherit = getAttributeValue(ancestor, "hx-inherit"); + if (initialElement !== ancestor) { + if (htmx.config.disableInheritance) { + if ( + inherit && + (inherit === "*" || inherit.split(" ").indexOf(attributeName) >= 0) + ) { + return attributeValue; + } else { + return null; + } + } + if ( + disinherit && + (disinherit === "*" || + disinherit.split(" ").indexOf(attributeName) >= 0) + ) { + return "unset"; + } + } + return attributeValue; + } + + /** + * @param {Element} elt + * @param {string} attributeName + * @returns {string | null} + */ + function getClosestAttributeValue(elt, attributeName) { + let closestAttr = null; + getClosestMatch(elt, function (e) { + return !!(closestAttr = getAttributeValueWithDisinheritance( + elt, + asElement(e), + attributeName, + )); + }); + if (closestAttr !== "unset") { + return closestAttr; + } + } + + /** + * @param {Node} elt + * @param {string} selector + * @returns {boolean} + */ + function matches(elt, selector) { + // @ts-ignore: non-standard properties for browser compatibility + // noinspection JSUnresolvedVariable + const matchesFunction = + elt instanceof Element && + (elt.matches || + elt.matchesSelector || + elt.msMatchesSelector || + elt.mozMatchesSelector || + elt.webkitMatchesSelector || + elt.oMatchesSelector); + return !!matchesFunction && matchesFunction.call(elt, selector); + } + + /** + * @param {string} str + * @returns {string} + */ + function getStartTag(str) { + const tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i; + const match = tagMatcher.exec(str); + if (match) { + return match[1].toLowerCase(); + } else { + return ""; + } + } + + /** + * @param {string} resp + * @returns {Document} + */ + function parseHTML(resp) { + const parser = new DOMParser(); + return parser.parseFromString(resp, "text/html"); + } + + /** + * @param {DocumentFragment} fragment + * @param {Node} elt + */ + function takeChildrenFor(fragment, elt) { + while (elt.childNodes.length > 0) { + fragment.append(elt.childNodes[0]); + } + } + + /** + * @param {HTMLScriptElement} script + * @returns {HTMLScriptElement} + */ + function duplicateScript(script) { + const newScript = getDocument().createElement("script"); + forEach(script.attributes, function (attr) { + newScript.setAttribute(attr.name, attr.value); + }); + newScript.textContent = script.textContent; + newScript.async = false; + if (htmx.config.inlineScriptNonce) { + newScript.nonce = htmx.config.inlineScriptNonce; + } + return newScript; + } + + /** + * @param {HTMLScriptElement} script + * @returns {boolean} + */ + function isJavaScriptScriptNode(script) { + return ( + script.matches("script") && + (script.type === "text/javascript" || + script.type === "module" || + script.type === "") + ); + } + + /** + * we have to make new copies of script tags that we are going to insert because + * SOME browsers (not saying who, but it involves an element and an animal) don't + * execute scripts created in