code:
/**
 * @callback passageLinkHandler
 * @returns {void}
 */
/**
 * Creates a HTML element with custom SugarCube attributes which works as a passage link
 *
 * The result works in the same way as the wiki markup in the SugarCube
 * @see https://www.motoslave.net/sugarcube/2/docs/#markup-html-attribute
 * @param {string} linkText link text
 * @param {string} passage the passage name to link to
 * @param {passageLinkHandler} [handler] setter text (optional)
 * @param {string} [tooltip=''] tooltip text (optional)
 * @param {string} [elementType='a'] element type (optional) default is 'a'.
 * Could be any of 'a', 'audio', img', 'source', 'video'
 * @returns {HTMLElement} element text
 *
 * @example
 * // equal to [[Go to town|Town]]
 * App.UI.passageLink("Go to town", "Town")
 */
App.UI.DOM.passageLink = function(linkText, passage, handler, tooltip = '', elementType = 'a') {
	let res = document.createElement(elementType);
	res.setAttribute("data-passage", passage);
	res.onclick = (ev) => {
		ev.preventDefault();
		if (handler) {
			handler();
		}
		Engine.play(passage);
	};

	if (tooltip) {
		res.title = tooltip;
	}
	res.textContent = linkText;
	return res;
};

/**
 * Returns link element for an assignment
 * @param {App.Entity.SlaveState} slave
 * @param {string} assignment
 * @param {string} [passage] passage to go to
 * @param {assignmentCallback} [action] action that changes slave state. The default one is a call to assignJob()
 * @param {string} [linkText]
 * @returns {HTMLAnchorElement}
 */
App.UI.DOM.assignmentLink = function(slave, assignment, passage, action, linkText) {
	let res = document.createElement("a");
	res.textContent = linkText;
	res.onclick = (e) => {
		e.preventDefault();
		if (action) {
			action(slave, assignment);
		} else {
			assignJob(slave, assignment);
		}
		if (passage !== '') {
			SugarCube.Engine.play(passage);
		}
	};
	return res;
};

App.UI.DOM.link = function() {
	return makeLink;

	/**
	 * Creates a markup for a SugarCube link which executes given function with given arguments
	 *
	 * @param {string} linkText link text
	 * @param {*} handler callable object
	 * @param {*} args arguments
	 * @param {string} [passage] the passage name to link to
	 * @returns {HTMLAnchorElement} link in SC markup
	 */
	function makeLink(linkText, handler, args = [], passage = '', tooltip = '') {
		const hArgs = Array.isArray(args) ? args : [args];
		let link = document.createElement("a");
		link.textContent = linkText;
		link.title = tooltip;
		link.onclick = () => {
			handler(...hArgs);
			if (passage !== '') {
				SugarCube.Engine.play(passage);
			}
		};
		return link;
	}
}();

/**
 * Creates a span for an link with tooltip containing the reasons why it is disabled
 * @param {string} link
 * @param {string[]} reasons
 * @returns {HTMLSpanElement}
 */
App.UI.DOM.disabledLink = function(link, reasons) {
	/** @type {HTMLElement} */
	let tooltip;
	if (reasons.length === 1) {
		tooltip = document.createElement("span");
		tooltip.textContent = reasons[0];
	} else {
		tooltip = document.createElement("div");
		let ul = document.createElement("ul");
		tooltip.appendChild(ul);
		for (const li of reasons.map(r => {
			const li = document.createElement("li");
			li.textContent = r;
			return li;
		})) {
			ul.appendChild(li);
		}
	}
	tooltip.className = "tooltip";
	let res = document.createElement("span");
	res.textContent = link;
	res.className = "textWithTooltip";
	res.appendChild(tooltip);
	return res;
};

/**
 * @param {string} text
 * @param {string|string[]} [classNames]
 */
App.UI.DOM.makeSpan = function makeSpan(text, classNames) {
	let r = document.createElement("span");
	if (classNames !== undefined) {
		if (Array.isArray(classNames)) {
			r.classList.add(...classNames);
		} else {
			r.classList.add(classNames);
		}
	}
	r.innerHTML = text;
	return r;
};

/**
 * @param {string} passage
 * @returns {Element}
 */
App.Utils.passageElement = function(passage) {
	return document.querySelector(`tw-passagedata[name="${passage}"]`);
};

/**
 * @param {Node[]} nodes
 * @returns {DocumentFragment}
 */
App.UI.DOM.combineNodes = function(...nodes) {
	let res = document.createDocumentFragment();
	for (const n of nodes) {
		res.appendChild(n);
	}
	return res;
};

/**
 * @param {string} text
 * @returns {HTMLElement}
 */
App.Utils.htmlToElement = function(text) {
	const template = document.createElement("template");
	text = text.trim(); // Never return a text node of whitespace as the result
	template.innerHTML = text;
	// @ts-ignore
	return template.content.firstChild;
};

/*
 * Shows tooltips if they are enabled. Must be called on every passage load, because <body> gets reset.
 */
$(document).on(":passageend", () => {
	if (V.tooltipsEnabled === 1) {
		document.body.classList.add("tooltipsEnabled");
	}
});