PageSourceSearch

https://mountaincabinsutah.com/wp-content/plugins/homerunner-toolkit/assets/js/edms-empty.js?ver=3.309.18

js mountaincabinsutah.com collected 2026-10-10 02:02:18 UTC 7,646 bytes, 228 lines download raw bytes

1/**
2 * Explorer Dynamic Min Stay — empty-state decision logic.
3 *
4 * Pure functions extracted so the logic is testable in Node
5 * (tests/js/edms-empty.test.mjs), same contract as psn-money.js:
6 * UMD-ish wrapper, ES5 syntax, dependency-free. Query parsing rides on
7 * URL/URLSearchParams (baseline in every browser that runs the
8 * Explorer, and in Node) — hand-rolled regex parsing diverged from
9 * canonical parsing on duplicate params and on `?checkin=` sequences
10 * embedded raw inside another parameter's value.
11 *
12 * The question this module answers: given the localized min-stay
13 * aggregate and the checkin/checkout a visitor arrived with in the URL,
14 * is a zero-results Explorer search GUARANTEED to be a min-stay
15 * failure? The aggregate holds MIN(min_stay) across the inventory per
16 * date, so "searched nights < aggregate[checkin]" means no property
17 * accepts that stay length on that date — the only case where the
18 * empty state may claim a min-stay cause instead of the generic text.
19 */
20(function (root, factory) {
21	if (typeof module === 'object' && module.exports) {
22		module.exports = factory();
23	} else {
24		root.hrtkEdmsEmpty = factory();
25	}
26})(typeof self !== 'undefined' ? self : this, function () {
27
28	var ISO_RE = /^\d{4}-\d{2}-\d{2}$/;
29	var DAY_MS = 86400000;
30
31	// Query keys PHP would fold into the canonical date params while
32	// URLSearchParams keys them separately: bracketed array variants
33	// (checkout[]=x becomes $_GET['checkout'] as an array) and
34	// NUL-truncated names (checkin%00x becomes $_GET['checkin'],
35	// last-wins — verified against parse_str). Their presence means the
36	// server-side reading is unknowable.
37	var SMUGGLED_KEY_RE = /^(checkin|checkout)[\[\u0000]/;
38
39	/**
40	 * First value of a query parameter from a search string; '' when
41	 * absent. Canonical URLSearchParams semantics, not regex.
42	 */
43	function getParam(search, name) {
44		return new URLSearchParams(search || '').get(name) || '';
45	}
46
47	/**
48	 * UTC midnight for a strict YYYY-MM-DD string; NaN when the string
49	 * is malformed or names an impossible date (2026-02-31 would
50	 * silently roll over to March in Date arithmetic — reject it).
51	 */
52	function utcMidnight(iso) {
53		if (typeof iso !== 'string' || !ISO_RE.test(iso)) {
54			return NaN;
55		}
56		var y = parseInt(iso.slice(0, 4), 10);
57		var mo = parseInt(iso.slice(5, 7), 10);
58		var d = parseInt(iso.slice(8, 10), 10);
59		var ms = Date.UTC(y, mo - 1, d);
60		var check = new Date(ms);
61		if (check.getUTCFullYear() !== y || check.getUTCMonth() !== mo - 1 || check.getUTCDate() !== d) {
62			return NaN;
63		}
64		return ms;
65	}
66
67	/**
68	 * Whole nights between two ISO dates via UTC math (immune to DST
69	 * transitions in the viewer's timezone). NaN on invalid input.
70	 */
71	function nightsBetween(checkin, checkout) {
72		var a = utcMidnight(checkin);
73		var b = utcMidnight(checkout);
74		if (isNaN(a) || isNaN(b)) {
75			return NaN;
76		}
77		return Math.round((b - a) / DAY_MS);
78	}
79
80	/** ISO date n days after the given ISO date; null on invalid input. */
81	function addDays(iso, n) {
82		var ms = utcMidnight(iso);
83		if (isNaN(ms) || typeof n !== 'number' || isNaN(n)) {
84			return null;
85		}
86		var d = new Date(ms + n * DAY_MS);
87		var mo = String(d.getUTCMonth() + 1);
88		var day = String(d.getUTCDate());
89		if (mo.length < 2) {
90			mo = '0' + mo;
91		}
92		if (day.length < 2) {
93			day = '0' + day;
94		}
95		return d.getUTCFullYear() + '-' + mo + '-' + day;
96	}
97
98	/**
99	 * {checkin, checkout} from a location.search string when EXACTLY ONE
100	 * of each is present and well-formed; null otherwise. Duplicates are
101	 * rejected outright: PHP resolves duplicate params last-wins while
102	 * URLSearchParams.get() is first-wins, so a duplicated date param
103	 * means we cannot know which value the search actually used — an
104	 * uncertain case, and uncertain cases make no claim.
105	 */
106	function parseSearch(search) {
107		var sp = new URLSearchParams(search || '');
108		var smuggled = false;
109		sp.forEach(function (value, key) {
110			if (SMUGGLED_KEY_RE.test(key)) {
111				smuggled = true;
112			}
113		});
114		if (smuggled) {
115			return null;
116		}
117		var checkins = sp.getAll('checkin');
118		var checkouts = sp.getAll('checkout');
119		if (checkins.length !== 1 || checkouts.length !== 1) {
120			return null;
121		}
122		if (isNaN(utcMidnight(checkins[0])) || isNaN(utcMidnight(checkouts[0]))) {
123			return null;
124		}
125		return { checkin: checkins[0], checkout: checkouts[0] };
126	}
127
128	/**
129	 * Strict integer reading of an aggregate value. localized JSON may
130	 * legitimately stringify ints ("2"), but anything fractional,
131	 * suffixed, or non-scalar is malformed data and must yield NaN so
132	 * the caller makes no claim ("2junk" and "2.9" both parseInt to 2).
133	 */
134	function toMinStay(v) {
135		if (typeof v === 'number') {
136			return isFinite(v) && Math.floor(v) === v ? v : NaN;
137		}
138		if (typeof v === 'string' && /^\d+$/.test(v)) {
139			return parseInt(v, 10);
140		}
141		return NaN;
142	}
143
144	/**
145	 * Decide whether a zero-results search for [checkin, checkout) is
146	 * guaranteed to be a min-stay failure.
147	 *
148	 * `uncertain` is the REQUIRED certainty contract: an object mapping
149	 * dates whose aggregate value is fleet-incomplete (some available
150	 * property carried no known minimum — Aggregator::compute_uncertain).
151	 * It is null when the last aggregation could not certify complete
152	 * data, and this function FAILS CLOSED on anything but an object:
153	 * without certified data, a min-stay claim could misattribute an
154	 * empty result that another filter caused while an
155	 * invisible-to-the-aggregate property accepts the short stay.
156	 *
157	 * @return null when we cannot make the claim (no certainty data,
158	 *         uncertain checkin date, no aggregate entry, invalid range,
159	 *         or the stay already meets the minimum — the empty result
160	 *         then has some other cause and the generic message must
161	 *         stand). Otherwise { nights, required, suggestedCheckout }.
162	 */
163	function evaluate(aggregate, checkin, checkout, uncertain) {
164		if (!aggregate || typeof aggregate !== 'object') {
165			return null;
166		}
167		if (!uncertain || typeof uncertain !== 'object') {
168			return null;
169		}
170		var nights = nightsBetween(checkin, checkout);
171		if (isNaN(nights) || nights < 1) {
172			return null;
173		}
174		if (Object.prototype.hasOwnProperty.call(uncertain, checkin)) {
175			return null;
176		}
177		if (!Object.prototype.hasOwnProperty.call(aggregate, checkin)) {
178			return null;
179		}
180		var required = toMinStay(aggregate[checkin]);
181		if (isNaN(required) || required <= 0 || required > 100) {
182			return null;
183		}
184		if (nights >= required) {
185			return null;
186		}
187		return {
188			nights: nights,
189			required: required,
190			suggestedCheckout: addDays(checkin, required),
191		};
192	}
193
194	/**
195	 * Rewrite the checkout parameter in a full (absolute) URL,
196	 * preserving every other parameter and any #fragment. Appends when
197	 * absent; collapses duplicate checkout params (searchParams.set
198	 * semantics), so the resubmitted search is unambiguous.
199	 */
200	function withCheckout(href, checkout) {
201		var u = new URL(href);
202		// Belt-and-braces: strip smuggled date-key variants (bracketed or
203		// NUL-truncated) so the retried search can't inherit a key
204		// parseSearch would reject.
205		var stale = [];
206		u.searchParams.forEach(function (value, key) {
207			if (SMUGGLED_KEY_RE.test(key)) {
208				stale.push(key);
209			}
210		});
211		stale.forEach(function (key) {
212			u.searchParams.delete(key);
213		});
214		u.searchParams.set('checkout', checkout);
215		return u.toString();
216	}
217
218	return {
219		getParam: getParam,
220		utcMidnight: utcMidnight,
221		nightsBetween: nightsBetween,
222		addDays: addDays,
223		toMinStay: toMinStay,
224		parseSearch: parseSearch,
225		evaluate: evaluate,
226		withCheckout: withCheckout,
227	};
228});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.