code:
/***********************************************************************************************************************

	lib/simplestore/adapters/FCHost.Storage.js

	Copyright Â© 2013â€“2019 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/
/* global SimpleStore, Util */

SimpleStore.adapters.push((() => {
	'use strict';

	// Adapter readiness state.
	let _ok = false;

	/*******************************************************************************************************************
		_FCHostStorageAdapter Class.
        Note that FCHost is only intended for a single document, so we ignore both prefixing and storageID
	*******************************************************************************************************************/
	class _FCHostStorageAdapter {
		constructor(persistent) {
			let engine = null;
			let name   = null;

			if (persistent) {
				engine = window.FCHostPersistent;
				name   = 'FCHostPersistent';
			}
			else {
			    engine = window.FCHostSession;
				name   = 'FCHostSession';
			}

			Object.defineProperties(this, {
				_engine : {
					value : engine
				},

				name : {
					value : name
				},

				persistent : {
					value : !!persistent
				}
			});
		}

		/* legacy */
		get length() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.length : Number]`); }

			return this._engine.size();
		}
		/* /legacy */

		size() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.size() : Number]`); }

			return this._engine.size();
		}

		keys() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.keys() : String Array]`); }

			return this._engine.keys();
		}

		has(key) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.has(key: "${key}") : Boolean]`); }

			if (typeof key !== 'string' || !key) {
				return false;
			}

			return this._engine.has(key);
		}

		get(key) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.get(key: "${key}") : Any]`); }

			if (typeof key !== 'string' || !key) {
				return null;
			}

			const value = this._engine.get(key);

			return value == null ? null : _FCHostStorageAdapter._deserialize(value); // lazy equality for null
		}

		set(key, value) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.set(key: "${key}", value: \u2026) : Boolean]`); }

			if (typeof key !== 'string' || !key) {
				return false;
			}

			this._engine.set(key, _FCHostStorageAdapter._serialize(value));

			return true;
		}

		delete(key) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.delete(key: "${key}") : Boolean]`); }

			if (typeof key !== 'string' || !key) {
				return false;
			}

			this._engine.remove(key);

			return true;
		}

		clear() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.clear() : Boolean]`); }

			this._engine.clear();

			return true;
		}

		static _serialize(obj) {
			return JSON.stringify(obj);
		}

		static _deserialize(str) {
			return JSON.parse(str);
		}
	}

	/*******************************************************************************************************************
		Adapter Utility Functions.
	*******************************************************************************************************************/
	function adapterInit() {
		// FCHost feature test.
		function hasFCHostStorage() {
			try {
			    if (typeof window.FCHostPersistent !== 'undefined')
			        return true;
			}
			catch (ex) { /* no-op */ }

			return false;
		}

		_ok = hasFCHostStorage();

		return _ok;
	}

	function adapterCreate(storageId, persistent) {
		if (!_ok) {
			throw new Error('adapter not initialized');
		}

		return new _FCHostStorageAdapter(persistent);
	}

	/*******************************************************************************************************************
		Module Exports.
	*******************************************************************************************************************/
	return Object.freeze(Object.defineProperties({}, {
		init   : { value : adapterInit },
		create : { value : adapterCreate }
	}));
})());


code:
/***********************************************************************************************************************

	lib/simplestore/adapters/cookie.js

	Copyright © 2013–2021 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/
/* global SimpleStore, Util */

SimpleStore.adapters.push((() => {
	'use strict';

	// Expiry constants.
	const _MAX_EXPIRY = 'Tue, 19 Jan 2038 03:14:07 GMT'; // (new Date((Math.pow(2, 31) - 1) * 1000)).toUTCString()
	const _MIN_EXPIRY = 'Thu, 01 Jan 1970 00:00:00 GMT'; // (new Date(0)).toUTCString()

	// Adapter readiness state.
	let _ok = false;

	/*******************************************************************************************************************
		_CookieAdapter Class.
	*******************************************************************************************************************/
	class _CookieAdapter {
		constructor(storageId, persistent) {
			const prefix = `${storageId}${persistent ? '!' : '*'}.`;

			Object.defineProperties(this, {
				_prefix : {
					value : prefix
				},

				_prefixRe : {
					value : new RegExp(`^${RegExp.escape(prefix)}`)
				},

				name : {
					value : 'cookie'
				},

				id : {
					value : storageId
				},

				persistent : {
					value : !!persistent
				}
			});
		}

		/* legacy */
		get length() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.length : Number]`); }

			return this.keys().length;
		}
		/* /legacy */

		size() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.size() : Number]`); }

			return this.keys().length;
		}

		keys() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.keys() : String Array]`); }

			if (document.cookie === '') {
				return [];
			}

			const cookies = document.cookie.split(/;\s*/);
			const keys    = [];

			for (let i = 0; i < cookies.length; ++i) {
				const kvPair = cookies[i].split('=');
				const key    = decodeURIComponent(kvPair[0]);

				if (this._prefixRe.test(key)) {
					/*
						All stored values are serialized and an empty string serializes to a non-empty
						string.  Therefore, receiving an empty string here signifies a deleted value,
						not a serialized empty string, so we should omit such pairs.
					*/
					const value = decodeURIComponent(kvPair[1]);

					if (value !== '') {
						keys.push(key.replace(this._prefixRe, ''));
					}
				}
			}

			return keys;
		}

		has(key) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.has(key: "${key}") : Boolean]`); }

			if (typeof key !== 'string' || !key) {
				return false;
			}

			return _CookieAdapter._getCookie(this._prefix + key) !== null;
		}

		get(key) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.get(key: "${key}") : Any]`); }

			if (typeof key !== 'string' || !key) {
				return null;
			}

			const value = _CookieAdapter._getCookie(this._prefix + key);

			return value === null ? null : _CookieAdapter._deserialize(value);
		}

		set(key, value) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.set(key: "${key}", value: \u2026) : Boolean]`); }

			if (typeof key !== 'string' || !key) {
				return false;
			}

			try {
				_CookieAdapter._setCookie(
					this._prefix + key,
					_CookieAdapter._serialize(value),

					// An undefined expiry denotes a session cookie.
					this.persistent ? _MAX_EXPIRY : undefined
				);

				if (!this.has(key)) {
					throw new Error('unknown validation error during set');
				}
			}
			catch (ex) {
				// Massage the cookie exception into something a bit nicer for the player.
				throw Util.newExceptionFrom(ex, Error, `cookie error: ${ex.message}`);
			}

			return true;
		}

		delete(key) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.delete(key: "${key}") : Boolean]`); }

			/*
				Attempting to delete a cookie implies setting it, so we test for its existence
				beforehand, to avoid creating it in the event that it does not already exist.
			*/
			if (typeof key !== 'string' || !key || !this.has(key)) {
				return false;
			}

			try {
				_CookieAdapter._setCookie(
					this._prefix + key,

					// Use `undefined` as the value.
					undefined,

					// Use the epoch as the expiry.
					_MIN_EXPIRY
				);

				if (this.has(key)) {
					throw new Error('unknown validation error during delete');
				}
			}
			catch (ex) {
				// Massage the cookie exception into something a bit nicer for the player.
				throw Util.newExceptionFrom(ex, Error, `cookie error: ${ex.message}`);
			}

			return true;
		}

		clear() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.clear() : Boolean]`); }

			const keys = this.keys();

			for (let i = 0, iend = keys.length; i < iend; ++i) {
				if (DEBUG) { console.log('\tdeleting key:', keys[i]); }

				this.delete(keys[i]);
			}

			// this.keys().forEach(key => {
			// 	if (DEBUG) { console.log('\tdeleting key:', key); }
			//
			// 	this.delete(key);
			// });

			return true;
		}

		static _getCookie(prefixedKey) {
			if (!prefixedKey || document.cookie === '') {
				return null;
			}

			const cookies = document.cookie.split(/;\s*/);

			for (let i = 0; i < cookies.length; ++i) {
				const kvPair = cookies[i].split('=');
				const key    = decodeURIComponent(kvPair[0]);

				if (prefixedKey === key) {
					const value = decodeURIComponent(kvPair[1]);

					/*
						All stored values are serialized and an empty string serializes to a non-empty
						string.  Therefore, receiving an empty string here signifies a deleted value,
						not a serialized empty string, so we should yield `null` for such pairs.
					*/
					return value || null;
				}
			}

			return null;
		}

		static _setCookie(prefixedKey, value, expiry) {
			if (!prefixedKey) {
				return;
			}

			let payload = `${encodeURIComponent(prefixedKey)}=`;

			if (value != null) { // lazy equality for null
				payload += encodeURIComponent(value);
			}

			if (expiry != null) { // lazy equality for null
				payload += `; expires=${expiry}`;
			}

			payload += '; path=/';
			document.cookie = payload;
		}

		static _serialize(obj) {
			// return LZString.compressToBase64(JSON.stringify(obj)); // TODO keep LZString a config option?
			return JSON.stringify(obj);
		}

		static _deserialize(str) {
			// return JSON.parse(LZString.decompressFromBase64(str));
			return JSON.parse(str);
		}
	}

	/*******************************************************************************************************************
		Adapter Utility Functions.
	*******************************************************************************************************************/
	function adapterInit(
		// Only used for stores updates.
		storageId
	) {
		// Cookie feature test.
		try {
			const tid = `_sc_${String(Date.now())}`;

			// We only test a session cookie as that should suffice.
			_CookieAdapter._setCookie(tid, _CookieAdapter._serialize(tid), undefined);
			_ok = _CookieAdapter._deserialize(_CookieAdapter._getCookie(tid)) === tid;
			_CookieAdapter._setCookie(tid, undefined, _MIN_EXPIRY);
		}
		catch (ex) {
			_ok = false;
		}

		/* legacy */
		// Attempt to update the cookie stores, if necessary.  This should happen only during initialization.
		if (_ok) {
			_updateCookieStores(storageId);
		}
		/* /legacy */

		return _ok;
	}

	function adapterCreate(storageId, persistent) {
		if (!_ok) {
			throw new Error('adapter not initialized');
		}

		return new _CookieAdapter(storageId, persistent);
	}

	/* legacy */
	// Updates old non-segmented cookie stores into segmented stores.
	function _updateCookieStores(storageId) {
		if (document.cookie === '') {
			return;
		}

		const oldPrefix     = `${storageId}.`;
		const oldPrefixRe   = new RegExp(`^${RegExp.escape(oldPrefix)}`);
		const persistPrefix = `${storageId}!.`;
		const sessionPrefix = `${storageId}*.`;
		const sessionTestRe = /\.(?:state|rcWarn)$/;
		const cookies       = document.cookie.split(/;\s*/);

		for (let i = 0; i < cookies.length; ++i) {
			const kvPair = cookies[i].split('=');
			const key    = decodeURIComponent(kvPair[0]);

			if (oldPrefixRe.test(key)) {
				/*
					All stored values are serialized and an empty string serializes to a non-empty
					string.  Therefore, receiving an empty string here signifies a deleted value,
					not a serialized empty string, so we should skip processing such pairs.
				*/
				const value = decodeURIComponent(kvPair[1]);

				if (value !== '') {
					const persist = !sessionTestRe.test(key);

					// Delete the old k/v pair.
					_CookieAdapter._setCookie(
						key,
						undefined,
						_MIN_EXPIRY
					);

					// Set the new k/v pair.
					_CookieAdapter._setCookie(
						key.replace(oldPrefixRe, () => persist ? persistPrefix : sessionPrefix),
						value,
						persist ? _MAX_EXPIRY : undefined
					);
				}
			}
		}
	}
	/* /legacy */

	/*******************************************************************************************************************
		Module Exports.
	*******************************************************************************************************************/
	return Object.freeze(Object.defineProperties({}, {
		init   : { value : adapterInit },
		create : { value : adapterCreate }
	}));
})());


code:
/***********************************************************************************************************************

	lib/simplestore/adapters/webstorage.js

	Copyright © 2013–2021 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/
/* global SimpleStore, Util, Config */

SimpleStore.adapters.push((() => {
	'use strict';

	// Adapter readiness state.
	let _ok = false;

	/*******************************************************************************************************************
		_WebStorageAdapter Class.
	*******************************************************************************************************************/
	class _WebStorageAdapter {
		constructor(storageId, persistent) {
			const prefix = `${storageId}.`;
			let engine = null;
			let name   = null;

			if (persistent) {
				engine = window.localStorage;
				name   = 'localStorage';
			}
			else {
				engine = window.sessionStorage;
				name   = 'sessionStorage';
			}

			Object.defineProperties(this, {
				_engine : {
					value : engine
				},

				_prefix : {
					value : prefix
				},

				_prefixRe : {
					value : new RegExp(`^${RegExp.escape(prefix)}`)
				},

				name : {
					value : name
				},

				id : {
					value : storageId
				},

				persistent : {
					value : !!persistent
				}
			});
		}

		/* legacy */
		get length() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.length : Number]`); }

			/*
				NOTE: DO NOT do something like `return this._engine.length;` here,
				as that will return the length of the entire store, rather than
				just our prefixed keys.
			*/
			return this.keys().length;
		}
		/* /legacy */

		size() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.size() : Number]`); }

			/*
				NOTE: DO NOT do something like `return this._engine.length;` here,
				as that will return the length of the entire store, rather than
				just our prefixed keys.
			*/
			return this.keys().length;
		}

		keys() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.keys() : String Array]`); }

			const keys = [];

			for (let i = 0; i < this._engine.length; ++i) {
				const key = this._engine.key(i);

				if (this._prefixRe.test(key)) {
					keys.push(key.replace(this._prefixRe, ''));
				}
			}

			return keys;
		}

		has(key) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.has(key: "${key}") : Boolean]`); }

			if (typeof key !== 'string' || !key) {
				return false;
			}

			// // FIXME: This method should probably check for the key, rather than comparing its value.
			// return this._engine.getItem(this._prefix + key) != null; // lazy equality for null

			return this._engine.hasOwnProperty(this._prefix + key);
		}

		get(key) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.get(key: "${key}") : Any]`); }

			if (typeof key !== 'string' || !key) {
				return null;
			}

			const value = this._engine.getItem(this._prefix + key);

			return value == null ? null : _WebStorageAdapter._deserialize(value); // lazy equality for null
		}

		set(key, value) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.set(key: "${key}", value: \u2026) : Boolean]`); }

			if (typeof key !== 'string' || !key) {
				return false;
			}

			try {
				this._engine.setItem(this._prefix + key, _WebStorageAdapter._serialize(value));
			}
			catch (ex) {
				/*
					If the exception is a quota exceeded error, massage it into something
					a bit nicer for the player.

					NOTE: Ideally, we could simply do something like checking `ex.code`, but
					it's a non-standard property and not supported in all browsers.  Thus,
					we have to resort to pattern matching the name and message—the latter being
					required by Opera (Presto).  I hate the parties responsible for this snafu
					so much.
				*/
				if (/quota.?(?:exceeded|reached)/i.test(ex.name + ex.message)) {
					throw Util.newExceptionFrom(ex, Error, `${this.name} quota exceeded`);
				}

				throw ex;
			}

			return true;
		}

		delete(key) {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.delete(key: "${key}") : Boolean]`); }

			if (typeof key !== 'string' || !key) {
				return false;
			}

			this._engine.removeItem(this._prefix + key);

			return true;
		}

		clear() {
			if (DEBUG) { console.log(`[<SimpleStore:${this.name}>.clear() : Boolean]`); }

			const keys = this.keys();

			for (let i = 0, iend = keys.length; i < iend; ++i) {
				if (DEBUG) { console.log('\tdeleting key:', keys[i]); }

				this.delete(keys[i]);
			}

			// return this.keys().forEach(key => {
			// 	if (DEBUG) { console.log('\tdeleting key:', key); }
			//
			// 	this.delete(key);
			// });

			return true;
		}

		static _serialize(obj) {
			if (Config.saves.useLZString === 1) return LZString.compressToUTF16(JSON.stringify(obj));
			return JSON.stringify(obj);
		}

		static _deserialize(str) {
			let parsedObj = null;
			// parse as JSON string by default, switch to LZString
			try {
				parsedObj = JSON.parse(str);
				if (Config.saves.useLZString === -1) Config.saves.useLZString = 0;
			}
			catch (ex) {
				parsedObj = JSON.parse(LZString.decompressFromUTF16(str));
				if (Config.saves.useLZString === -1) Config.saves.useLZString = 1;
			}
			return parsedObj;
		}
	}

	/*******************************************************************************************************************
		Adapter Utility Functions.
	*******************************************************************************************************************/
	function adapterInit() {
		// Web Storage feature test.
		function hasWebStorage(storeId) {
			try {
				const store = window[storeId];
				const tid   = `_sc_${String(Date.now())}`;
				store.setItem(tid, tid);
				const result = store.getItem(tid) === tid;
				store.removeItem(tid);
				return result;
			}
			catch (ex) { /* no-op */ }

			return false;
		}

		/*
			Just to be safe, we feature test for both `localStorage` and `sessionStorage`,
			as you never know what browser implementation bugs you're going to run into.
		*/
		_ok = hasWebStorage('localStorage') && hasWebStorage('sessionStorage');

		return _ok;
	}

	function adapterCreate(storageId, persistent) {
		if (!_ok) {
			throw new Error('adapter not initialized');
		}

		return new _WebStorageAdapter(storageId, persistent);
	}

	/*******************************************************************************************************************
		Module Exports.
	*******************************************************************************************************************/
	return Object.freeze(Object.defineProperties({}, {
		init   : { value : adapterInit },
		create : { value : adapterCreate }
	}));
})());


code:
/***********************************************************************************************************************

	lib/stylewrapper.js

	Copyright © 2013–2021 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/
/* global Patterns, Story, Wikifier */

var StyleWrapper = (() => { // eslint-disable-line no-unused-vars, no-var
	'use strict';

	const _imageMarkupRe    = new RegExp(Patterns.cssImage, 'g');
	const _hasImageMarkupRe = new RegExp(Patterns.cssImage);

	/*******************************************************************************************************************
		StyleWrapper Class.
	*******************************************************************************************************************/
	class StyleWrapper {
		constructor(style) {
			if (style == null) { // lazy equality for null
				throw new TypeError('StyleWrapper style parameter must be an HTMLStyleElement object');
			}

			Object.defineProperties(this, {
				style : {
					value : style
				}
			});
		}

		isEmpty() {
			// This should work in all supported browsers.
			return this.style.cssRules.length === 0;
		}

		set(rawCss) {
			this.clear();
			this.add(rawCss);
		}

		add(rawCss) {
			let css = rawCss;

			// Check for wiki image transclusion.
			if (_hasImageMarkupRe.test(css)) {
				/*
					The JavaScript specifications, since at least ES3, say that `<String>.replace()`
					should reset a global-flagged regular expression's `lastIndex` property to `0`
					upon invocation.  Buggy browser versions exist, however, which do not reset
					`lastIndex`, so we should do so manually to support those browsers.

					NOTE: I do not think this is actually necessary, since `_imageMarkupRe` is
					scoped to this module—meaning users should not be able to access it.  That
					being the case, and since we search to exhaustion which should also cause
					`lastIndex` to be reset, there should never be an instance where we invoke
					`css.replace()` and `_imageMarkupRe.lastIndex` is not already `0`.  Still,
					considering the other bug, better safe than sorry.
				*/
				_imageMarkupRe.lastIndex = 0;

				css = css.replace(_imageMarkupRe, wikiImage => {
					const markup = Wikifier.helpers.parseSquareBracketedMarkup({
						source     : wikiImage,
						matchStart : 0
					});

					if (markup.hasOwnProperty('error') || markup.pos < wikiImage.length) {
						return wikiImage;
					}

					let source = markup.source;

					// Handle image passage transclusion.
					if (source.slice(0, 5) !== 'data:' && Story.has(source)) {
						const passage = Story.get(source);

						if (passage.tags.includes('Twine.image')) {
							source = passage.text.trim();
						}
					}

					/*
						The source may be URI- or Base64-encoded, so we cannot use `encodeURIComponent()`
						here.  Instead, we simply encode any double quotes, since the URI will be
						delimited by them.
					*/
					return `url("${source.replace(/"/g, '%22')}")`;
				});
			}

			// For IE ≤ 10.
			if (this.style.styleSheet) {
				this.style.styleSheet.cssText += css;
			}

			// For all other browsers (incl. IE ≥ 11).
			else {
				this.style.appendChild(document.createTextNode(css));
			}
		}

		clear() {
			// For IE ≤10.
			if (this.style.styleSheet) {
				this.style.styleSheet.cssText = '';
			}

			// For all other browsers (incl. IE ≥11).
			else {
				jQuery(this.style).empty();
			}
		}
	}

	/*******************************************************************************************************************
		Module Exports.
	*******************************************************************************************************************/
	return StyleWrapper;
})();


code:
/***********************************************************************************************************************

	lib/util.js

	Copyright © 2013–2021 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/
/* global Has, Scripting */

var Util = (() => { // eslint-disable-line no-unused-vars, no-var
	'use strict';

	/*******************************************************************************************************************
		Type Functions.
	*******************************************************************************************************************/
	/*
		Returns the value yielded by `typeof` (for primitives), the `@@toStringTag`
		internal property (for objects), and `'null'` for `null`.

		NOTE: In ≤ES5, returns the value of the `[[Class]]` internal slot for objects.
	*/
	const utilGetType = (() => {
		// Cache the `<Object>.toString()` method.
		const toString = Object.prototype.toString;

		// If the browser is using the `Map()` and `Set()` polyfills, then return
		// a version of `utilGetType()` that contains special cases for them, since
		// they do not have a `[[Class]]` internal slot and the `@@toStringTag`
		// internal property is unavailable to them.
		if (toString.call(new Map()) === '[object Object]') {
			return function utilGetType(O) {
				if (O === null) { return 'null'; }

				// Special cases for the `Map` and `Set` polyfills.
				//
				// NOTE: We don't special case the `WeakMap` and `WeakSet` polyfills
				// here since they're (a) unlikely to be used and (b) broken anyway.
				if (O instanceof Map) { return 'Map'; }
				if (O instanceof Set) { return 'Set'; }

				const baseType = typeof O;
				return baseType === 'object' ? toString.call(O).slice(8, -1) : baseType;
			};
		}

		// Elsewise, return the regular `utilGetType()` function.
		return function utilGetType(O) {
			if (O === null) { return 'null'; }

			const baseType = typeof O;
			return baseType === 'object' ? toString.call(O).slice(8, -1) : baseType;
		};
	})();

	/*
		Returns whether the passed value is a boolean or one of the strings "true"
		or "false".
	*/
	function utilIsBoolean(obj) {
		return typeof obj === 'boolean' || typeof obj === 'string' && (obj === 'true' || obj === 'false');
	}

	/*
		Returns whether the passed value is iterable.
	*/
	function utilIsIterable(obj) {
		return obj != null && typeof obj[Symbol.iterator] === 'function'; // lazy equality for null
	}

	/*
		Returns whether the passed value is a finite number or a numeric string which
		yields a finite number when parsed.
	*/
	function utilIsNumeric(obj) {
		let num;

		switch (typeof obj) {
		case 'number':
			num = obj;
			break;

		case 'string':
			num = Number(obj);
			break;

		default:
			return false;
		}

		return !Number.isNaN(num) && Number.isFinite(num);
	}

	/*
		Returns whether the passed values pass a SameValueZero comparison.

		SEE: http://ecma-international.org/ecma-262/8.0/#sec-samevaluezero
	*/
	function utilSameValueZero(a, b) {
		/*
			NOTE: This comparison could also be implemented thus:

				```
				a === b ||
				typeof a === 'number' && typeof b === 'number' &&
				Number.isNaN(a) && Number.isNaN(b)
				```

			That's needlessly verbose, however, as `NaN` is the only value in
			the language which is not reflexive.
		*/
		return a === b || a !== a && b !== b;
	}

	/*
		Returns a pseudo-enumeration created from the given Array, Map, Set, or generic object.
	*/
	function utilToEnum(obj) {
		const pEnum = Object.create(null);

		if (obj instanceof Array) {
			obj.forEach((val, i) => pEnum[String(val)] = i);
		}
		else if (obj instanceof Set) {
			// NOTE: Use `<Array>.forEach()` here rather than `<Set>.forEach()`
			// as the latter does not provide the indices we require.
			Array.from(obj).forEach((val, i) => pEnum[String(val)] = i);
		}
		else if (obj instanceof Map) {
			obj.forEach((val, key) => pEnum[String(key)] = val);
		}
		else if (
			   typeof obj === 'object'
			&& obj !== null
			&& Object.getPrototypeOf(obj) === Object.prototype
		) {
			Object.assign(pEnum, obj);
		}
		else {
			throw new TypeError('Util.toEnum obj parameter must be an Array, Map, Set, or generic object');
		}

		return Object.freeze(pEnum);
	}

	/*
		Returns the value of the `@@toStringTag` property of the given object.

		NOTE: In ≤ES5, returns the value of the `[[Class]]` internal slot.
	*/
	function utilToStringTag(obj) {
		return Object.prototype.toString.call(obj).slice(8, -1);
	}

	/*******************************************************************************************************************
		String Encoding Functions.
	*******************************************************************************************************************/
	/*
		Returns a trimmed and encoded slug of the passed string that should be safe
		for use as a DOM ID or class name.

		NOTE: The range of illegal characters consists of: C0 controls, space, exclamation,
		double quote, number, dollar, percent, ampersand, single quote, left paren, right
		paren, asterisk, plus, comma, hyphen, period, forward slash, colon, semi-colon,
		less-than, equals, greater-than, question, at, left bracket, backslash, right
		bracket, caret, backquote/grave, left brace, pipe/vertical-bar, right brace, tilde,
		delete, C1 controls.
	*/
	const _illegalSlugCharsRe = /[\x00-\x20!-/:-@[-^`{-\x9f]+/g; // eslint-disable-line no-control-regex
	/* legacy */
	const _isInvalidSlugRe = /^-*$/; // Matches the empty string or one comprised solely of hyphens.
	/* /legacy */

	function utilSlugify(str) {
		const base = String(str).trim();

		/* legacy */
		const _legacy = base
			.replace(/[^\w\s\u2013\u2014-]+/g, '')
			.replace(/[_\s\u2013\u2014-]+/g, '-')
			.toLocaleLowerCase();

		if (!_isInvalidSlugRe.test(_legacy)) {
			return _legacy;
		}
		/* /legacy */

		return base
			.replace(_illegalSlugCharsRe, '')
			.replace(/[_\s\u2013\u2014-]+/g, '-');

		// For v3.
		// return base.replace(_illegalSlugCharsRe, '-');
	}

	/*
		Returns a sanitized version of the given string that should be safe for use
		as a filename under both Windows and Unix-based/-like operating systems.

		NOTE: The range of illegal characters consists of: C0 controls, double quote,
		number, dollar, percent, ampersand, single quote, asterisk, plus, comma,
		forward slash, colon, semi-colon, less-than, equals, greater-than, question,
		backslash, caret, backquote/grave, pipe/vertical line, delete, C1 controls.
	*/
	const _illegalFilenameCharsRE = /[\x00-\x1f"#$%&'*+,/:;<=>?\\^`|\x7f-\x9f]+/g; // eslint-disable-line no-control-regex

	function utilSanitizeFilename(str) {
		return String(str).trim()
			.replace(_illegalFilenameCharsRE, '');
	}

	/*
		Returns an entity encoded version of the passed string.

		NOTE: Escapes the five primary HTML special characters, the backquote,
		and SugarCube markup metacharacters.
	*/
	const _markupCharsRe    = /[!"#$&'*\-/<=>?@[\\\]^_`{|}~]/g;
	const _hasMarkupCharsRe = new RegExp(_markupCharsRe.source); // to drop the global flag
	const _markupCharsMap   = utilToEnum({
		/* eslint-disable quote-props */
		'!'  : '!',
		'"'  : '"',
		'#'  : '#',
		'$'  : '$',
		'&'  : '&',
		"'"  : ''',
		'*'  : '*',
		'-'  : '-',
		'/'  : '/',
		'<'  : '<',
		'='  : '=',
		'>'  : '>',
		'?'  : '?',
		'@'  : '@',
		'['  : '[',
		'\\' : '\',
		']'  : ']',
		'^'  : '^',
		'_'  : '_',
		'`'  : '`',
		'{'  : '{',
		'|'  : '|',
		'}'  : '}',
		'~'  : '~'
		/* eslint-enable quote-props */
	});

	function utilEscapeMarkup(str) {
		if (str == null) { // lazy equality for null
			return '';
		}

		const val = String(str);
		return val && _hasMarkupCharsRe.test(val)
			? val.replace(_markupCharsRe, ch => _markupCharsMap[ch])
			: val;
	}

	/*
		Returns an entity encoded version of the passed string.

		NOTE: Only escapes the five primary special characters and the backquote.
	*/
	const _htmlCharsRe    = /[&<>"'`]/g;
	const _hasHtmlCharsRe = new RegExp(_htmlCharsRe.source); // to drop the global flag
	const _htmlCharsMap   = utilToEnum({
		'&' : '&',
		'<' : '<',
		'>' : '>',
		'"' : '"',
		"'" : ''',
		'`' : '`'
	});

	function utilEscape(str) {
		if (str == null) { // lazy equality for null
			return '';
		}

		const val = String(str);
		return val && _hasHtmlCharsRe.test(val)
			? val.replace(_htmlCharsRe, ch => _htmlCharsMap[ch])
			: val;
	}

	/*
		Returns a decoded version of the passed entity encoded string.

		NOTE: The extended replacement set here, in contrast to `utilEscape()`,
		is required due to observed stupidity from various sources.
	*/
	const _escapedHtmlRe    = /&(?:amp|#38|#x26|lt|#60|#x3c|gt|#62|#x3e|quot|#34|#x22|apos|#39|#x27|#96|#x60);/gi;
	const _hasEscapedHtmlRe = new RegExp(_escapedHtmlRe.source, 'i'); // to drop the global flag
	const _escapedHtmlMap   = utilToEnum({
		'&'  : '&', // ampersand (HTML character entity, XML predefined entity)
		'&'  : '&', // ampersand (decimal numeric character reference)
		'&' : '&', // ampersand (hexadecimal numeric character reference)
		'<'   : '<', // less-than (HTML character entity, XML predefined entity)
		'<'  : '<', // less-than (decimal numeric character reference)
		'<' : '<', // less-than (hexadecimal numeric character reference)
		'>'   : '>', // greater-than (HTML character entity, XML predefined entity)
		'>'  : '>', // greater-than (decimal numeric character reference)
		'>' : '>', // greater-than (hexadecimal numeric character reference)
		'"' : '"', // double quote (HTML character entity, XML predefined entity)
		'"'  : '"', // double quote (decimal numeric character reference)
		'"' : '"', // double quote (hexadecimal numeric character reference)
		''' : "'", // apostrophe (XML predefined entity)
		'''  : "'", // apostrophe (decimal numeric character reference)
		''' : "'", // apostrophe (hexadecimal numeric character reference)
		'`'  : '`', // backquote (decimal numeric character reference)
		'`' : '`'  // backquote (hexadecimal numeric character reference)
	});

	function utilUnescape(str) {
		if (str == null) { // lazy equality for null
			return '';
		}

		const val = String(str);
		return val && _hasEscapedHtmlRe.test(val)
			? val.replace(_escapedHtmlRe, entity => _escapedHtmlMap[entity.toLowerCase()])
			: val;
	}

	/*
		Returns an object (`{ char, start, end }`) containing the Unicode character at
		position `pos`, its starting position, and its ending position—surrogate pairs
		are properly handled.  If `pos` is out-of-bounds, returns an object containing
		the empty string and start/end positions of `-1`.

		This function is necessary because JavaScript strings are sequences of UTF-16
		code units, so surrogate pairs are exposed and thus must be handled.  While the
		ES6/2015 standard does improve the situation somewhat, it does not alleviate
		the need for this function.

		NOTE: Returns the individual code units of invalid surrogate pairs as-is.

		IDEA: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt
	*/
	function utilCharAndPosAt(text, position) {
		const str  = String(text);
		const pos  = Math.trunc(position);
		const code = str.charCodeAt(pos);

		// Given position was out-of-bounds.
		if (Number.isNaN(code)) {
			return { char : '', start : -1, end : -1 };
		}

		const retval = {
			char  : str.charAt(pos),
			start : pos,
			end   : pos
		};

		// Code unit is not a UTF-16 surrogate.
		if (code < 0xD800 || code > 0xDFFF) {
			return retval;
		}

		// Code unit is a high surrogate (D800–DBFF).
		if (code >= 0xD800 && code <= 0xDBFF) {
			const nextPos = pos + 1;

			// End of string.
			if (nextPos >= str.length) {
				return retval;
			}

			const nextCode = str.charCodeAt(nextPos);

			// Next code unit is not a low surrogate (DC00–DFFF).
			if (nextCode < 0xDC00 || nextCode > 0xDFFF) {
				return retval;
			}

			retval.char = retval.char + str.charAt(nextPos);
			retval.end = nextPos;
			return retval;
		}

		// Code unit is a low surrogate (DC00–DFFF) in the first position.
		if (pos === 0) {
			return retval;
		}

		const prevPos  = pos - 1;
		const prevCode = str.charCodeAt(prevPos);

		// Previous code unit is not a high surrogate (D800–DBFF).
		if (prevCode < 0xD800 || prevCode > 0xDBFF) {
			return retval;
		}

		retval.char = str.charAt(prevPos) + retval.char;
		retval.start = prevPos;
		return retval;
	}

	/*******************************************************************************************************************
		Time Functions.
	*******************************************************************************************************************/
	/*
		Returns the number of milliseconds elapsed since a reference epoch.

		NOTE: Use the Performance API, if available, elsewise use Date as a
		failover.  The Performance API is preferred for its monotonic clock—
		meaning, it's not subject to the vagaries of timezone changes and leap
		periods, as is Date.
	*/
	const _nowSource = Has.performance ? performance : Date;

	function utilNow() {
		return _nowSource.now();
	}

	/*******************************************************************************************************************
		Conversion Functions.
	*******************************************************************************************************************/
	/*
		Returns the number of miliseconds represented by the passed CSS time string.
	*/
	const _cssTimeRe = /^([+-]?(?:\d*\.)?\d+)([Mm]?[Ss])$/;

	function utilFromCssTime(cssTime) {
		const match = _cssTimeRe.exec(String(cssTime));

		if (match === null) {
			throw new SyntaxError(`invalid time value syntax: "${cssTime}"`);
		}

		let msec = Number(match[1]);

		if (match[2].length === 1) {
			msec *= 1000;
		}

		if (Number.isNaN(msec) || !Number.isFinite(msec)) {
			throw new RangeError(`invalid time value: "${cssTime}"`);
		}

		return msec;
	}

	/*
		Returns the CSS time string represented by the passed number of milliseconds.
	*/
	function utilToCssTime(msec) {
		if (typeof msec !== 'number' || Number.isNaN(msec) || !Number.isFinite(msec)) {
			let what;

			switch (typeof msec) {
			case 'string':
				what = `"${msec}"`;
				break;

			case 'number':
				what = String(msec);
				break;

			default:
				what = utilToStringTag(msec);
				break;
			}

			throw new Error(`invalid milliseconds: ${what}`);
		}

		return `${msec}ms`;
	}

	/*
		Returns the DOM property name represented by the passed CSS property name.
	*/
	function utilFromCssProperty(cssName) {
		if (!cssName.includes('-')) {
			switch (cssName) {
			case 'bgcolor': return 'backgroundColor';
			case 'float':   return 'cssFloat';
			default:        return cssName;
			}
		}

		// Strip the leading hyphen from the `-ms-` vendor prefix, so it stays lowercased.
		const normalized = cssName.slice(0, 4) === '-ms-' ? cssName.slice(1) : cssName;

		return normalized
			.split('-')
			.map((part, i) => i === 0 ? part : part.toUpperFirst())
			.join('');
	}

	/*
		Returns an object containing the component properties parsed from the passed URL.
	*/
	function utilParseUrl(url) {
		const el       = document.createElement('a');
		const queryObj = Object.create(null);

		// Let the `<a>` element parse the URL.
		el.href = url;

		// Populate the `queryObj` object with the query string attributes.
		if (el.search) {
			el.search
				.replace(/^\?/, '')
				.splitOrEmpty(/(?:&(?:amp;)?|;)/)
				.forEach(query => {
					const [key, value] = query.split('=');
					queryObj[key] = value;
				});
		}

		/*
			Caveats by browser:
				Edge and Internet Explorer (≥8) do not support authentication
				information within a URL at all and will throw a security exception
				on *any* property access if it's included.

				Internet Explorer does not include the leading forward slash on
				`pathname` when required.

				Opera (Presto) strips the authentication information from `href`
				and does not supply `username` or `password`.

				Safari (ca. v5.1.x) does not supply `username` or `password` and
				peforms URI decoding on `pathname`.
		*/

		// Patch for IE not including the leading slash on `pathname` when required.
		const pathname = el.host && el.pathname[0] !== '/' ? `/${el.pathname}` : el.pathname;

		return {
			// The full URL that was originally parsed.
			href : el.href,

			// The request protocol, lowercased.
			protocol : el.protocol,

			// // The full authentication information.
			// auth : el.username || el.password // eslint-disable-line no-nested-ternary
			// 	? `${el.username}:${el.password}`
			// 	: typeof el.username === 'string' ? '' : undefined,
			//
			// // The username portion of the auth info.
			// username : el.username,
			//
			// // The password portion of the auth info.
			// password : el.password,

			// The full host information, including port number, lowercased.
			host : el.host,

			// The hostname portion of the host info, lowercased.
			hostname : el.hostname,

			// The port number portion of the host info.
			port : el.port,

			// The full path information, including query info.
			path : `${pathname}${el.search}`,

			// The pathname portion of the path info.
			pathname,

			// The query string portion of the path info, including the leading question mark.
			query  : el.search,
			search : el.search,

			// The attributes portion of the query string, parsed into an object.
			queries  : queryObj,
			searches : queryObj,

			// The fragment string, including the leading hash/pound sign.
			hash : el.hash
		};
	}

	/*
		Returns a new exception based on the given exception.

		NOTE: Mostly useful for making a standard JavaScript exception type copy
		of a host exception type—e.g. `DOMException` → `Error`.
	*/
	function utilNewExceptionFrom(original, exceptionType, override) {
		if (typeof original !== 'object' || original === null) {
			throw new Error('Util.newExceptionFrom original parameter must be an object');
		}
		if (typeof exceptionType !== 'function') {
			throw new Error('Util.newExceptionFrom exceptionType parameter must be an error type constructor');
		}

		const ex = new exceptionType(original.message); // eslint-disable-line new-cap

		if (typeof original.name !== 'undefined') {
			ex.name = original.name;
		}
		if (typeof original.code !== 'undefined') {
			ex.code = original.code;
		}
		if (typeof original.columnNumber !== 'undefined') {
			ex.columnNumber = original.columnNumber;
		}
		if (typeof original.description !== 'undefined') {
			ex.description = original.description;
		}
		if (typeof original.fileName !== 'undefined') {
			ex.fileName = original.fileName;
		}
		if (typeof original.lineNumber !== 'undefined') {
			ex.lineNumber = original.lineNumber;
		}
		if (typeof original.number !== 'undefined') {
			ex.number = original.number;
		}
		if (typeof original.stack !== 'undefined') {
			ex.stack = original.stack;
		}

		const overrideType = typeof override;

		if (overrideType !== 'undefined') {
			if (overrideType === 'object' && override !== null) {
				Object.assign(ex, override);
			}
			else if (overrideType === 'string') {
				ex.message = override;
			}
			else {
				throw new Error('Util.newExceptionFrom override parameter must be an object or string');
			}
		}

		return ex;
	}

	/*
		Returns a sanitized version of the passed `KeyboardEvent.key` value from
		previous incarnations of the specification that should better reflect the
		current incarnation.
	*/
	const utilScrubEventKey = (() => {
		let separatorKey;
		let decimalKey;

		// Attempt to determine the player's 'Separator' and 'Decimal' key values
		// based on their current locale.
		if (typeof Intl !== 'undefined' && typeof Intl.NumberFormat === 'function') {
			const match = new Intl.NumberFormat().format(111111.5).match(/(\D*)\d+(\D*)/);

			if (match) {
				separatorKey = match[1];
				decimalKey   = match[2];
			}
		}

		// Failover to US-centric values, if using `Intl.NumberFormat` failed.
		if (!separatorKey && !decimalKey) {
			separatorKey = ',';
			decimalKey   = '.';
		}

		// Maps older `KeyboardEvent.key` values to more current/correct ones.
		function utilScrubEventKey(key) {
			switch (key) {
			// case 'OS':                 return 'Meta'; // Unreliable.
			case 'Scroll':             return 'ScrollLock';
			case 'Spacebar':           return '\x20';
			case 'Left':               return 'ArrowLeft';
			case 'Right':              return 'ArrowRight';
			case 'Up':                 return 'ArrowUp';
			case 'Down':               return 'ArrowDown';
			case 'Del':                return 'Delete';
			case 'Crsel':              return 'CrSel';
			case 'Exsel':              return 'ExSel';
			case 'Esc':                return 'Escape';
			case 'Apps':               return 'ContextMenu';
			case 'Nonconvert':         return 'NonConvert';
			case 'MediaNextTrack':     return 'MediaTrackNext';
			case 'MediaPreviousTrack': return 'MediaTrackPrevious';
			case 'VolumeUp':           return 'AudioVolumeUp';
			case 'VolumeDown':         return 'AudioVolumeDown';
			case 'VolumeMute':         return 'AudioVolumeMute';
			case 'Zoom':               return 'ZoomToggle';
			case 'SelectMedia':        /* see below */
			case 'MediaSelect':        return 'LaunchMediaPlayer';
			case 'Add':                return '+';
			case 'Divide':             return '/';
			case 'Multiply':           return '*';
			case 'Subtract':           return '-';
			case 'Decimal':            return decimalKey;
			case 'Separator':          return separatorKey;
			}

			return key;
		}

		return utilScrubEventKey;
	})();

	/*******************************************************************************************************************
		Browser API Functions.
	*******************************************************************************************************************/
	/*
		Returns whether the given media query matches.
	*/
	const utilHasMediaQuery = (() => {
		// If the browser does not support `matchMedia()`, then return
		// a version of `utilHasMediaQuery()` that simply returns `false`.
		if (typeof window.matchMedia !== 'function') {
			return function utilHasMediaQuery() {
				return false;
			};
		}

		// Elsewise, return the regular `utilHasMediaQuery()` function.
		return function utilHasMediaQuery(mediaQuery) {
			return window.matchMedia(mediaQuery).matches;
		};
	})();

	/*******************************************************************************************************************
		Module Exports.
	*******************************************************************************************************************/
	return Object.freeze(Object.defineProperties({}, {
		/*
			Type Functions.
		*/
		getType       : { value : utilGetType },
		isBoolean     : { value : utilIsBoolean },
		isIterable    : { value : utilIsIterable },
		isNumeric     : { value : utilIsNumeric },
		sameValueZero : { value : utilSameValueZero },
		toEnum        : { value : utilToEnum },
		toStringTag   : { value : utilToStringTag },

		/*
			String Encoding Functions.
		*/
		slugify          : { value : utilSlugify },
		sanitizeFilename : { value : utilSanitizeFilename },
		escapeMarkup     : { value : utilEscapeMarkup },
		escape           : { value : utilEscape },
		unescape         : { value : utilUnescape },
		charAndPosAt     : { value : utilCharAndPosAt },

		/*
			Time Functions.
		*/
		now : { value : utilNow },

		/*
			Conversion Functions.
		*/
		fromCssTime      : { value : utilFromCssTime },
		toCssTime        : { value : utilToCssTime },
		fromCssProperty  : { value : utilFromCssProperty },
		parseUrl         : { value : utilParseUrl },
		newExceptionFrom : { value : utilNewExceptionFrom },
		scrubEventKey    : { value : utilScrubEventKey },

		/*
			Browser API Functions.
		*/
		hasMediaQuery : { value : utilHasMediaQuery },

		/*
			Legacy Aliases.
		*/
		random         : { value : Math.random },
		entityEncode   : { value : utilEscape },
		entityDecode   : { value : utilUnescape },
		evalExpression : { value : (...args) => Scripting.evalJavaScript(...args) }, // SEE: `markup/scripting.js`.
		evalStatements : { value : (...args) => Scripting.evalJavaScript(...args) }  // SEE: `markup/scripting.js`.
	}));
})();


code:
/***********************************************************************************************************************

	lib/visibility.js

	Copyright © 2018–2021 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/

var Visibility = (() => { // eslint-disable-line no-unused-vars, no-var
	'use strict';

	/*
		There are two versions of the Page Visibility API: First Edition and, the current,
		Second Edition (i.e. "Level 2").  First Edition is mentioned here only because some
		older browsers implement it, rather than the current specification.

		SEE:
			Second Edition : https://www.w3.org/TR/page-visibility/
			First Edition  : https://www.w3.org/TR/2013/REC-page-visibility-20130514/

		NOTE: Generally, all supported browsers change the visibility state when either switching tabs
		within the browser or minimizing the browser window.  Exceptions are noted below:
			* IE 9 doesn't support either version of the Page Visibility API.
			* Opera 12 (Presto) doesn't change the visibility state when the browser is minimized.
	*/

	// Vendor properties object.
	const vendor = (() => {
		try {
			return Object.freeze([
				// Specification.
				{
					hiddenProperty : 'hidden',          // boolean; historical in 2nd edition
					stateProperty  : 'visibilityState', // string, values: 'hidden', 'visible'; 1st edition had more values
					changeEvent    : 'visibilitychange'
				},

				// `webkit` prefixed: old Blink & WebKit.
				{
					hiddenProperty : 'webkitHidden',
					stateProperty  : 'webkitVisibilityState',
					changeEvent    : 'webkitvisibilitychange'
				},

				// `moz` prefixed: old Gecko, maybe Seamonkey.
				{
					hiddenProperty : 'mozHidden',
					stateProperty  : 'mozVisibilityState',
					changeEvent    : 'mozvisibilitychange'
				},

				// `ms` prefixed: IE 10.
				{
					hiddenProperty : 'msHidden',
					stateProperty  : 'msVisibilityState',
					changeEvent    : 'msvisibilitychange'
				}
			].find(vnd => vnd.hiddenProperty in document));
		}
		catch (ex) { /* no-op */ }

		return undefined;
	})();

	/*******************************************************************************
		API Functions.
	*******************************************************************************/

	function getVendor() {
		return vendor;
	}

	function getVisibility() {
		return vendor && document[vendor.stateProperty] || 'visible';
	}

	function isEnabled() {
		return Boolean(vendor);
	}

	function isHidden() {
		// return Boolean(vendor && document[vendor.stateProperty] === 'hidden');
		return Boolean(vendor && document[vendor.hiddenProperty]); // NOTE: Historical, but probably better for 1st edition.
	}

	/*******************************************************************************
		Module Exports.
	*******************************************************************************/

	return Object.freeze(Object.defineProperties({}, {
		// Functions.
		vendor    : { get : getVendor },
		state     : { get : getVisibility },
		isEnabled : { value : isEnabled },
		isHidden  : { value : isHidden },

		// Properties.
		hiddenProperty : { value : vendor && vendor.hiddenProperty },
		stateProperty  : { value : vendor && vendor.stateProperty },
		changeEvent    : { value : vendor && vendor.changeEvent }
	}));
})();


code:
/***********************************************************************************************************************

	loadscreen.js

	Copyright © 2013–2021 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/
/* global Config, Engine */

var LoadScreen = (() => { // eslint-disable-line no-unused-vars, no-var
	'use strict';

	// Locks collection.
	const _locks = new Set();

	// Auto-incrementing lock ID.
	let _autoId = 0;

	/*******************************************************************************************************************
		LoadScreen Functions.
	*******************************************************************************************************************/
	/*
		Initialize management of the loading screen.
	*/
	function loadScreenInit() {
		if (DEBUG) { console.log('[LoadScreen/loadScreenInit()]'); }

		// Add a `readystatechange` listener for hiding/showing the loading screen.
		jQuery(document).on('readystatechange.SugarCube', () => {
			if (DEBUG) { console.log(`[LoadScreen/<readystatechange>] document.readyState: "${document.readyState}"; locks(${_locks.size}):`, _locks); }

			if (_locks.size > 0) {
				return;
			}

			// The value of `document.readyState` may be: 'loading' -> 'interactive' -> 'complete'.
			// Though, to reach this point, it must already be in, at least, the 'interactive' state.
			if (document.readyState === 'complete') {
				if (jQuery(document.documentElement).attr('data-init') === 'loading') {
					if (Config.loadDelay > 0) {
						setTimeout(() => {
							if (_locks.size === 0) {
								loadScreenHide();
							}
						}, Math.max(Engine.minDomActionDelay, Config.loadDelay));
					}
					else {
						loadScreenHide();
					}
				}
			}
			else {
				loadScreenShow();
			}
		});
	}

	/*
		Clear the loading screen.
	*/
	function loadScreenClear() {
		if (DEBUG) { console.log('[LoadScreen/loadScreenClear()]'); }

		// Remove the event listener.
		jQuery(document).off('readystatechange.SugarCube');

		// Clear all locks.
		_locks.clear();

		// Hide the loading screen.
		loadScreenHide();
	}

	/*
		Hide the loading screen.
	*/
	function loadScreenHide() {
		if (DEBUG) { console.log('[LoadScreen/loadScreenHide()]'); }

		jQuery(document.documentElement).removeAttr('data-init');
	}

	/*
		Show the loading screen.
	*/
	function loadScreenShow() {
		if (DEBUG) { console.log('[LoadScreen/loadScreenShow()]'); }

		jQuery(document.documentElement).attr('data-init', 'loading');
	}

	/*
		Returns a new lock ID after locking and showing the loading screen.
	*/
	function loadScreenLock() {
		if (DEBUG) { console.log('[LoadScreen/loadScreenLock()]'); }

		++_autoId;
		_locks.add(_autoId);

		if (DEBUG) { console.log(`\tacquired loading screen lock; id: ${_autoId}`); }

		loadScreenShow();
		return _autoId;
	}

	/*
		Remove the lock associated with the given lock ID and, if no locks remain,
		trigger a `readystatechange` event.
	*/
	function loadScreenUnlock(id) {
		if (DEBUG) { console.log(`[LoadScreen/loadScreenUnlock(id: ${id})]`); }

		if (id == null) { // lazy equality for null
			throw new Error('LoadScreen.unlock called with a null or undefined ID');
		}

		if (_locks.has(id)) {
			_locks.delete(id);

			if (DEBUG) { console.log(`\treleased loading screen lock; id: ${id}`); }
		}

		if (_locks.size === 0) {
			jQuery(document).trigger('readystatechange');
		}
	}

	/*******************************************************************************************************************
		Module Exports.
	*******************************************************************************************************************/
	return Object.freeze(Object.defineProperties({}, {
		init   : { value : loadScreenInit },
		clear  : { value : loadScreenClear },
		hide   : { value : loadScreenHide },
		show   : { value : loadScreenShow },
		lock   : { value : loadScreenLock },
		unlock : { value : loadScreenUnlock }
	}));
})();


code:
/***********************************************************************************************************************

	macro/macros/unset.js

	Copyright © 2013–2023 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/
/* global Config, Macro, Scripting, getErrorMessage */

/*
	<<unset>>
*/
Macro.add('unset', {
	skipArgs : true,

	handler() {
		if (this.args.full.length === 0) {
			return this.error('no story/temporary variable list specified');
		}

		const searchRe  = /[,;\s]*((?:State\.(?:variables|temporary)|setup)\.)/g;
		const replacer  = (_, p1) => `; delete ${p1}`;
		const cleanupRe = /^; /;

		try {
			const unsetExp = this.args.full.replace(searchRe, replacer).replace(cleanupRe, '');

			Scripting.evalJavaScript(unsetExp);
		}
		catch (ex) {
			return this.error(`bad evaluation: ${getErrorMessage(ex)}`);
		}

		// Custom debug view setup.
		if (Config.debug) {
			this.debugView.modes({ hidden : true });
		}
	}
});


code:
/***********************************************************************************************************************

	macros/macro.js

	Copyright © 2013–2021 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/
/* global Patterns, Scripting, macros */

var Macro = (() => { // eslint-disable-line no-unused-vars, no-var
	'use strict';

	// Macro definitions.
	const _macros = {};

	// Map of all macro tags and their parents (key: 'tag name' => value: ['list of parent names']).
	const _tags = {};

	// Valid macro name regular expression.
	const _validNameRe = new RegExp(`^(?:${Patterns.macroName})$`);

	/*******************************************************************************************************************
		Macros Functions.
	*******************************************************************************************************************/
	function macrosAdd(name, def) {
		if (Array.isArray(name)) {
			name.forEach(name => macrosAdd(name, def));
			return;
		}

		if (!_validNameRe.test(name)) {
			throw new Error(`invalid macro name "${name}"`);
		}

		if (macrosHas(name)) {
			throw new Error(`cannot clobber existing macro <<${name}>>`);
		}
		else if (tagsHas(name)) {
			throw new Error(`cannot clobber child tag <<${name}>> of parent macro${_tags[name].length === 1 ? '' : 's'} <<${_tags[name].join('>>, <<')}>>`);
		}

		try {
			if (typeof def === 'object') {
				// Add the macro definition.
				//
				// NOTE: Since `macrosGet()` may return legacy macros, we add the `_MACRO_API`
				// flag to (modern) API macros, so that the macro formatter will know how to
				// call the macro.  This should be removed in v3.
				_macros[name] = Object.assign(Object.create(null), def, { _MACRO_API : true });
			}
			else {
				// Add the macro alias.
				if (macrosHas(def)) {
					_macros[name] = Object.create(_macros[def], {
						_ALIAS_OF : {
							enumerable : true,
							value      : def
						}
					});
				}
				else {
					throw new Error(`cannot create alias of nonexistent macro <<${def}>>`);
				}
			}

			Object.defineProperty(_macros, name, { writable : false });
		}
		catch (ex) {
			if (ex.name === 'TypeError') {
				throw new Error(`cannot clobber protected macro <<${name}>>`);
			}
			else {
				throw new Error(`unknown error when attempting to add macro <<${name}>>: [${ex.name}] ${ex.message}`);
			}
		}

		// Tags post-processing.
		if (typeof _macros[name].tags !== 'undefined') {
			if (_macros[name].tags == null) { // lazy equality for null
				tagsRegister(name);
			}
			else if (Array.isArray(_macros[name].tags)) {
				tagsRegister(name, _macros[name].tags);
			}
			else {
				throw new Error(`bad value for "tags" property of macro <<${name}>>`);
			}
		}
	}

	function macrosDelete(name) {
		if (Array.isArray(name)) {
			name.forEach(name => macrosDelete(name));
			return;
		}

		if (macrosHas(name)) {
			// Tags pre-processing.
			if (typeof _macros[name].tags !== 'undefined') {
				tagsUnregister(name);
			}

			try {
				// Remove the macro definition.
				Object.defineProperty(_macros, name, { writable : true });
				delete _macros[name];
			}
			catch (ex) {
				throw new Error(`unknown error removing macro <<${name}>>: ${ex.message}`);
			}
		}
		else if (tagsHas(name)) {
			throw new Error(`cannot remove child tag <<${name}>> of parent macro <<${_tags[name]}>>`);
		}
	}

	function macrosIsEmpty() {
		return Object.keys(_macros).length === 0;
	}

	function macrosHas(name) {
		return _macros.hasOwnProperty(name);
	}

	function macrosGet(name) {
		let macro = null;

		if (macrosHas(name) && typeof _macros[name].handler === 'function') {
			macro = _macros[name];
		}
		/* legacy macro support */
		else if (macros.hasOwnProperty(name) && typeof macros[name].handler === 'function') {
			macro = macros[name];
		}
		/* /legacy macro support */

		return macro;
	}

	function macrosInit(handler = 'init') { // eslint-disable-line no-unused-vars
		Object.keys(_macros).forEach(name => {
			if (typeof _macros[name][handler] === 'function') {
				_macros[name][handler](name);
			}
		});

		/* legacy macro support */
		Object.keys(macros).forEach(name => {
			if (typeof macros[name][handler] === 'function') {
				macros[name][handler](name);
			}
		});
		/* /legacy macro support */
	}

	/*******************************************************************************************************************
		Tags Functions.
	*******************************************************************************************************************/
	function tagsRegister(parent, bodyTags) {
		if (!parent) {
			throw new Error('no parent specified');
		}

		const endTags = [`/${parent}`, `end${parent}`]; // automatically create the closing tags
		const allTags = [].concat(endTags, Array.isArray(bodyTags) ? bodyTags : []);

		for (let i = 0; i < allTags.length; ++i) {
			const tag = allTags[i];

			if (macrosHas(tag)) {
				throw new Error('cannot register tag for an existing macro');
			}

			if (tagsHas(tag)) {
				if (!_tags[tag].includes(parent)) {
					_tags[tag].push(parent);
					_tags[tag].sort();
				}
			}
			else {
				_tags[tag] = [parent];
			}
		}
	}

	function tagsUnregister(parent) {
		if (!parent) {
			throw new Error('no parent specified');
		}

		Object.keys(_tags).forEach(tag => {
			const i = _tags[tag].indexOf(parent);

			if (i !== -1) {
				if (_tags[tag].length === 1) {
					delete _tags[tag];
				}
				else {
					_tags[tag].splice(i, 1);
				}
			}
		});
	}

	function tagsHas(name) {
		return _tags.hasOwnProperty(name);
	}

	function tagsGet(name) {
		return tagsHas(name) ? _tags[name] : null;
	}

	/*******************************************************************************************************************
		Module Exports.
	*******************************************************************************************************************/
	return Object.freeze(Object.defineProperties({}, {
		/*
			Macro Functions.
		*/
		add     : { value : macrosAdd },
		delete  : { value : macrosDelete },
		isEmpty : { value : macrosIsEmpty },
		has     : { value : macrosHas },
		get     : { value : macrosGet },
		init    : { value : macrosInit },

		/*
			Tags Functions.
		*/
		tags : {
			value : Object.freeze(Object.defineProperties({}, {
				register   : { value : tagsRegister },
				unregister : { value : tagsUnregister },
				has        : { value : tagsHas },
				get        : { value : tagsGet }
			}))
		},

		/*
			Legacy Aliases.
		*/
		evalStatements : { value : (...args) => Scripting.evalJavaScript(...args) } // SEE: `markup/scripting.js`.
	}));
})();


code:
/***********************************************************************************************************************

	macros/macrocontext.js

	Copyright © 2013–2021 Thomas Michael Edwards <thomasmedwards@gmail.com>. All rights reserved.
	Use of this source code is governed by a BSD 2-clause "Simplified" License, which may be found in the LICENSE file.

***********************************************************************************************************************/
/* global Config, DebugView, Patterns, State, Wikifier, throwError */

var MacroContext = (() => { // eslint-disable-line no-unused-vars, no-var
	'use strict';

	/*******************************************************************************************************************
		MacroContext Class.
	*******************************************************************************************************************/
	class MacroContext {
		constructor(contextData) {
			const context = Object.assign({
				parent      : null,
				macro       : null,
				name        : '',
				displayName : '',
				args        : null,
				payload     : null,
				parser      : null,
				source      : ''
			}, contextData);

			if (context.macro === null || context.name === '' || context.parser === null) {
				throw new TypeError('context object missing required properties');
			}

			Object.defineProperties(this, {
				self : {
					value : context.macro
				},

				name : {
					value : typeof context.macro._ALIAS_OF === 'undefined' ? context.name : context.macro._ALIAS_OF
				},

				displayName : {
					value : context.name
				},

				args : {
					value : context.args
				},

				payload : {
					value : context.payload
				},

				source : {
					value : context.source
				},

				parent : {
					value : context.parent
				},

				parser : {
					value : context.parser
				},

				_output : {
					value : context.parser.output
				},

				_shadows : {
					writable : true,
					value    : null
				},

				_debugView : {
					writable : true,
					value    : null
				},

				_debugViewEnabled : {
					writable : true,
					value    : Config.debug
				}
			});
		}

		get output() {
			return this._debugViewEnabled ? this.debugView.output : this._output;
		}

		get shadows() {
			return [...this._shadows];
		}

		get shadowView() {
			const view = new Set();
			this.contextSelectAll(ctx => ctx._shadows)
				.forEach(ctx => ctx._shadows.forEach(name => view.add(name)));
			return [...view];
		}

		get debugView() {
			if (this._debugViewEnabled) {
				return this._debugView !== null ? this._debugView : this.createDebugView();
			}

			return null;
		}

		contextHas(filter) {
			let context = this;

			while ((context = context.parent) !== null) {
				if (filter(context)) {
					return true;
				}
			}

			return false;
		}

		contextSelect(filter) {
			let context = this;

			while ((context = context.parent) !== null) {
				if (filter(context)) {
					return context;
				}
			}

			return null;
		}

		contextSelectAll(filter) {
			const result = [];
			let context = this;

			while ((context = context.parent) !== null) {
				if (filter(context)) {
					result.push(context);
				}
			}

			return result;
		}

		addShadow(...names) {
			if (!this._shadows) {
				this._shadows = new Set();
			}

			const varRe = new RegExp(`^${Patterns.variable}$`);

			names
				.flat(Infinity)
				.forEach(name => {
					if (typeof name !== 'string') {
						throw new TypeError(`variable name must be a string; type: ${typeof name}`);
					}

					if (!varRe.test(name)) {
						throw new Error(`invalid variable name "${name}"`);
					}

					this._shadows.add(name);
				});
		}

		createShadowWrapper(callback, doneCallback, startCallback) {
			const shadowContext = this;
			let shadowStore;

			if (typeof callback === 'function') {
				shadowStore = {};
				this.shadowView.forEach(varName => {
					const varKey = varName.slice(1);
					const store  = varName[0] === '$' ? State.variables : State.temporary;
					shadowStore[varName] = store[varKey];
				});
			}

			return function (...args) {
				if (typeof startCallback === 'function') {
					startCallback.apply(this, args);
				}

				if (typeof callback === 'function') {
					const shadowNames = Object.keys(shadowStore);
					const valueCache  = shadowNames.length > 0 ? {} : null;
					const macroParser = Wikifier.Parser.get('macro');
					let contextCache;

					/*
						There's no catch clause because this try/finally is here simply to ensure that
						proper cleanup is done in the event that an exception is thrown during the
						callback.
					*/
					try {
						/*
							Cache the existing values of the variables to be shadowed and assign the
							shadow values.
						*/
						shadowNames.forEach(varName => {
							const varKey = varName.slice(1);
							const store  = varName[0] === '$' ? State.variables : State.temporary;

							if (store.hasOwnProperty(varKey)) {
								valueCache[varKey] = store[varKey];
							}

							store[varKey] = shadowStore[varName];
						});

						// Cache the existing macro execution context and assign the shadow context.
						contextCache = macroParser.context;
						macroParser.context = shadowContext;

						// Call the callback function.
						callback.apply(this, args);
					}
					finally {
						// Revert the macro execution context shadowing.
						if (contextCache !== undefined) {
							macroParser.context = contextCache;
						}

						// Revert the variable shadowing.
						shadowNames.forEach(varName => {
							const varKey = varName.slice(1);
							const store  = varName[0] === '$' ? State.variables : State.temporary;

							/*
								Update the shadow store with the variable's current value, in case it
								was modified during the callback.
							*/
							shadowStore[varName] = store[varKey];

							if (valueCache.hasOwnProperty(varKey)) {
								store[varKey] = valueCache[varKey];
							}
							else {
								delete store[varKey];
							}
						});
					}
				}

				if (typeof doneCallback === 'function') {
					doneCallback.apply(this, args);
				}
			};
		}

		createDebugView(name, title) {
			this._debugView = new DebugView(
				this._output,
				'macro',
				name ? name : this.displayName,
				title ? title : this.source
			);

			if (this.payload !== null && this.payload.length > 0) {
				this._debugView.modes({ nonvoid : true });
			}

			this._debugViewEnabled = true;
			return this._debugView;
		}

		removeDebugView() {
			if (this._debugView !== null) {
				this._debugView.remove();
				this._debugView = null;
			}

			this._debugViewEnabled = false;
		}

		error(message, source, stack) {
			return throwError(this._output, `<<${this.displayName}>>: ${message}`, source ? source : this.source, stack);
		}
	}

	/*******************************************************************************************************************
		Module Exports.
	*******************************************************************************************************************/
	return MacroContext;
})();