@jaredwray/mockhttp 1.9.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +40 -8
  2. package/dist/index.d.mts +90 -11
  3. package/dist/index.mjs +203 -240
  4. package/package.json +17 -13
  5. package/site/dist/api/index.html +17983 -0
  6. package/site/dist/api/swagger.json +5487 -0
  7. package/site/dist/changelog/index.html +784 -0
  8. package/site/dist/changelog/page/2/index.html +577 -0
  9. package/site/dist/changelog/v0-2-0/index.html +437 -0
  10. package/site/dist/changelog/v0-2-1/index.html +438 -0
  11. package/site/dist/changelog/v0-2-2/index.html +436 -0
  12. package/site/dist/changelog/v0-2-3/index.html +440 -0
  13. package/site/dist/changelog/v0-3-0/index.html +443 -0
  14. package/site/dist/changelog/v0-4-0/index.html +437 -0
  15. package/site/dist/changelog/v0-4-1/index.html +441 -0
  16. package/site/dist/changelog/v0-4-2/index.html +446 -0
  17. package/site/dist/changelog/v0-4-3/index.html +437 -0
  18. package/site/dist/changelog/v0-5-0/index.html +443 -0
  19. package/site/dist/changelog/v0-6-0/index.html +444 -0
  20. package/site/dist/changelog/v0-6-1/index.html +441 -0
  21. package/site/dist/changelog/v0-7-0/index.html +445 -0
  22. package/site/dist/changelog/v0-8-0/index.html +440 -0
  23. package/site/dist/changelog/v0-9-0/index.html +437 -0
  24. package/site/dist/changelog/v1-0-0/index.html +446 -0
  25. package/site/dist/changelog/v1-0-1/index.html +437 -0
  26. package/site/dist/changelog/v1-1-0/index.html +445 -0
  27. package/site/dist/changelog/v1-2-0/index.html +443 -0
  28. package/site/dist/changelog/v1-3-0/index.html +448 -0
  29. package/site/dist/changelog/v1-4-0/index.html +445 -0
  30. package/site/dist/changelog/v1-5-0/index.html +439 -0
  31. package/site/dist/changelog/v1-5-1/index.html +443 -0
  32. package/site/dist/changelog/v1-6-0/index.html +467 -0
  33. package/site/dist/changelog/v1-7-0/index.html +462 -0
  34. package/site/dist/changelog/v1-8-0/index.html +481 -0
  35. package/site/dist/changelog/v1-8-1/index.html +457 -0
  36. package/site/dist/changelog/v1-9-0/index.html +495 -0
  37. package/site/dist/changelog/v2-0-0/index.html +500 -0
  38. package/site/dist/changelog/v2-1-0/index.html +476 -0
  39. package/site/dist/changelog-latest.json +269 -0
  40. package/site/dist/changelog.json +399 -0
  41. package/site/dist/css/api.css +1113 -0
  42. package/site/dist/css/highlight/highlight.min.js +1433 -0
  43. package/site/dist/css/highlight/styles/base16/docula.css +123 -0
  44. package/site/dist/css/home.css +133 -0
  45. package/site/dist/css/styles.css +1227 -0
  46. package/site/dist/css/variables.css +32 -0
  47. package/site/dist/docs/bins/index.html +980 -0
  48. package/site/dist/docs/configuration/index.html +809 -0
  49. package/site/dist/docs/hosted/index.html +583 -0
  50. package/site/dist/docs/how-to-deploy/cloudflare/index.html +655 -0
  51. package/site/dist/docs/how-to-deploy/docker/index.html +664 -0
  52. package/site/dist/docs/how-to-deploy/wasmer/index.html +625 -0
  53. package/site/dist/docs/https/index.html +796 -0
  54. package/site/dist/docs/index.html +640 -0
  55. package/site/dist/docs/library/index.html +818 -0
  56. package/site/dist/docs/taps/index.html +746 -0
  57. package/site/dist/favicon.svg +14 -0
  58. package/site/dist/feed.xml +1 -0
  59. package/site/dist/index.html +640 -0
  60. package/site/dist/js/api.js +363 -0
  61. package/site/dist/js/search.js +436 -0
  62. package/site/dist/llms-full.txt +7640 -0
  63. package/site/dist/llms.txt +43 -0
  64. package/site/dist/logo.svg +21 -0
  65. package/site/dist/robots.txt +2 -0
  66. package/site/dist/search-index.json +1 -0
  67. package/site/dist/sitemap.xml +1 -0
@@ -0,0 +1,495 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="description" content="HTTP mock server and httpbin replacement for API testing. Run it at mockhttp.org, in Docker, or in Node.js.">
8
+ <link rel="stylesheet" href="/css/variables.css">
9
+ <link rel="stylesheet" href="/css/styles.css">
10
+ <link rel="stylesheet" href="/css/highlight/styles/base16/docula.css">
11
+ <link rel="icon" href="/logo.svg">
12
+ <script>
13
+ (function(){
14
+ window.__doculaThemeKey = 'docula:theme:' + ('https://mockhttp.org').replace(/^https?:\/\//, '');
15
+ var mode = localStorage.getItem(window.__doculaThemeKey) || 'light';
16
+ var resolved = mode === 'system'
17
+ ? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
18
+ : mode;
19
+ if (resolved === 'light') document.documentElement.setAttribute('data-theme', 'light');
20
+ try {
21
+ var authState = JSON.parse(localStorage.getItem('docula-auth-state'));
22
+ if (authState) {
23
+ window.__doculaAuth = authState;
24
+ if (authState.loggedIn) {
25
+ document.documentElement.classList.add('docula-auth-logged-in');
26
+ }
27
+ }
28
+ } catch (e) {
29
+ // Ignore localStorage parsing errors
30
+ }
31
+ if (!window.__doculaAuth) {
32
+ window.__doculaAuth = { loggedIn: false, displayName: null };
33
+ }
34
+ })();
35
+ </script>
36
+ <title>MockHTTP - v1.9.0</title>
37
+ <script type="application/ld+json">
38
+ {"@context":"https://schema.org","@type":"BlogPosting","headline":"v1.9.0","description":"<p>Stable indexed response-format templates, plus security hardening and dependency refreshes.</p>\n<ul>\n<li>\n<p>add stable response format output via optional template index (#181)</p>\n<pre><code class=\"hljs language-bash\"><span class=\"hljs-comment\"># Same index → byte-identical JSON. Omit the index for the previous random pick.</span>\r\ncurl http://localhost:3000/json/1\r\ncurl http://localhost:3000/html/1\r\ncurl http://localhost:3000/json/99\r\n<span class=\"hljs-comment\"># → 404 {\"error\":\"Template index must be between 1 and 5\"}</span>\n</code></pre>\n</li>\n</ul>","url":"https://mockhttp.org/changelog/v1-9-0/","publisher":{"@type":"Organization","name":"MockHTTP"},"datePublished":"2026-08-19"}
39
+ </script>
40
+ </head>
41
+
42
+ <body>
43
+ <header class="header">
44
+ <div class="header-top">
45
+ <div class="header-top__content">
46
+ <button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle navigation menu">
47
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
48
+ </button>
49
+ <a href="/" class="logo-link">
50
+ <img alt="MockHTTP" class="logo__img" src="/logo.svg">
51
+ </a>
52
+ <nav class="header-bottom__nav">
53
+ <a class="header-bottom__item" href="/docs/" id="nav-docs">
54
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
55
+ <span>Documentation</span>
56
+ </a>
57
+ <a class="header-bottom__item" href="/api" id="nav-api">
58
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
59
+ <span>API Reference</span>
60
+ </a>
61
+ <a class="header-bottom__item" href="/changelog/" id="nav-changelog">
62
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"/><path d="M8 6v8"/></svg>
63
+ <span>Changelog</span>
64
+ </a>
65
+ </nav>
66
+ <div class="header-actions">
67
+ <button class="search-button" id="search-button" type="button" aria-label="Search" aria-keyshortcuts="Meta+K Control+K">
68
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
69
+ <span class="search-button__text">Search</span>
70
+ <span class="search-button__shortcut">⌘K</span>
71
+ </button>
72
+ <button class="theme-button" id="theme-toggle" aria-label="Theme: system (following system)">
73
+ <svg class="theme-button__icon theme-button__icon--system" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
74
+ <svg class="theme-button__icon theme-button__icon--sun theme-button__icon--hidden" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
75
+ <svg class="theme-button__icon theme-button__icon--moon theme-button__icon--hidden" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>
76
+ </button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </header>
81
+ <aside class="mobile-sidebar" id="mobile-sidebar">
82
+ <nav class="mobile-nav">
83
+ <a class="mobile-nav__item" href="/docs/">
84
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
85
+ <span>Documentation</span>
86
+ </a>
87
+ <a class="mobile-nav__item" href="/api">
88
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
89
+ <span>API Reference</span>
90
+ </a>
91
+ <a class="mobile-nav__item" href="/changelog/">
92
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"/><path d="M8 6v8"/></svg>
93
+ <span>Changelog</span>
94
+ </a>
95
+ </nav>
96
+ <div class="mobile-sidebar__docs" id="mobile-sidebar-docs"></div>
97
+ </aside>
98
+ <div class="sidebar-backdrop" id="sidebar-backdrop"></div>
99
+ <div class="search-modal" id="search-modal" hidden>
100
+ <div class="search-modal__backdrop" data-search-close></div>
101
+ <div class="search-modal__panel" role="dialog" aria-modal="true" aria-label="Search">
102
+ <div class="search-modal__header">
103
+ <svg class="search-modal__icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
104
+ <input
105
+ type="search"
106
+ id="search-input"
107
+ class="search-modal__input"
108
+ placeholder="Search"
109
+ autocomplete="off"
110
+ autocorrect="off"
111
+ autocapitalize="off"
112
+ spellcheck="false"
113
+ aria-label="Search"
114
+ aria-controls="search-results"
115
+ aria-activedescendant=""
116
+ role="combobox"
117
+ aria-expanded="true"
118
+ />
119
+ <button class="search-modal__clear" id="search-clear" type="button" aria-label="Clear search" hidden>
120
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
121
+ </button>
122
+ </div>
123
+ <div class="search-modal__body">
124
+ <ul class="search-results" id="search-results" role="listbox" aria-label="Search results"></ul>
125
+ <div class="search-modal__empty" id="search-empty" hidden>
126
+ No results for "<span class="search-modal__empty-query" id="search-empty-query"></span>"
127
+ </div>
128
+ <div class="search-modal__initial" id="search-initial">
129
+ Type to search the documentation.
130
+ </div>
131
+ </div>
132
+ <div class="search-modal__footer">
133
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">↑</kbd><kbd class="search-modal__kbd">↓</kbd> to navigate</span>
134
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">↵</kbd> to select</span>
135
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">esc</kbd> to close</span>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ <script>
140
+ window.__doculaSearch = { indexUrl: "/search-index.json" };
141
+ </script>
142
+ <script src="/js/search.js" defer></script>
143
+ <div class="content">
144
+ <main class="versions-container">
145
+ <div class="versions-content">
146
+ <div class="changelog-entry-nav">
147
+ <a href="/changelog/">&larr; Back to Changelog</a>
148
+ </div>
149
+ <div class="changelog-entry changelog-entry-single">
150
+ <div class="changelog-entry-header">
151
+ <h1 class="changelog-entry-title">v1.9.0</h1>
152
+ <span class="changelog-tag changelog-tag-release">Release</span>
153
+ </div>
154
+ <span class="changelog-entry-date">August 19, 2026</span>
155
+ <div class="changelog-entry-body">
156
+ <h2 id="jaredwraymockhttp190--2026-08-19">@jaredwray/mockhttp@1.9.0 — 2026-08-19</h2>
157
+ <p>Stable indexed response-format templates, plus security hardening and dependency refreshes.</p>
158
+ <h3 id="features">Features</h3>
159
+ <ul>
160
+ <li>
161
+ <p>add stable response format output via optional template index (#181)</p>
162
+ <pre><code class="hljs language-bash"><span class="hljs-comment"># Same index → byte-identical JSON. Omit the index for the previous random pick.</span>
163
+ curl http://localhost:3000/json/1
164
+ curl http://localhost:3000/html/1
165
+ curl http://localhost:3000/json/99
166
+ <span class="hljs-comment"># → 404 {"error":"Template index must be between 1 and 5"}</span>
167
+ </code></pre>
168
+ </li>
169
+ </ul>
170
+ <h3 id="bug-fixes">Bug Fixes</h3>
171
+ <ul>
172
+ <li>clone JSON template per request to protect stable output (#181)</li>
173
+ </ul>
174
+ <h3 id="internal">Internal</h3>
175
+ <ul>
176
+ <li>assert trailing-slash index requests fail loudly with 404 (#181)</li>
177
+ <li>land security docs through zizmor (#182)</li>
178
+ <li>stage npm publishes instead of publishing live (#190)</li>
179
+ <li>gate staged publishes on Aikido scan-release (#191)</li>
180
+ <li>prefer tilde ranges for runtime deps (#192)</li>
181
+ <li>record repository lockdown (#193)</li>
182
+ <li>remove set-cookie-parser override (#194)</li>
183
+ <li>remove @ungap/structured-clone override (#195)</li>
184
+ <li>remove glob override (#196)</li>
185
+ <li>upgrade @biomejs/biome 2.5.3 → 2.5.8 (#197)</li>
186
+ <li>upgrade TypeScript 6.0.3 → 7.0.2, tsdown 0.22.2 → 0.22.14, tsx 4.23.0 → 4.23.12, @swc/core 1.15.43 → 1.15.47 (#198)</li>
187
+ <li>upgrade package manager pnpm 11.20.0 → 11.21.0 (#200)</li>
188
+ <li>upgrade fastify 5.10.0 → 5.11.3, @fastify/cookie, @fastify/rate-limit, @fastify/static, @fastify/swagger, @fastify/swagger-ui (#201)</li>
189
+ <li>upgrade @scalar/api-reference 1.62.5 → 1.64.1 (#202)</li>
190
+ <li>upgrade hookified 3.0.1 → 3.0.2 (#203)</li>
191
+ <li>prefer caret ranges over tilde (#204)</li>
192
+ <li>pin Docker Node.js runtime image to node:24.19.0-alpine3.24 (#205)</li>
193
+ </ul>
194
+ <h3 id="contributors">Contributors</h3>
195
+ <ul>
196
+ <li>@jaredwray (15)</li>
197
+ </ul>
198
+ <h3 id="full-list-of-changes">Full List of Changes</h3>
199
+ <ul>
200
+ <li>feat: add stable response format output via optional template index by @jaredwray in #181</li>
201
+ <li>mockhttp - chore: defense - land security docs through zizmor by @jaredwray in #182</li>
202
+ <li>mockhttp - chore: defense - stage npm publishes instead of publishing live by @jaredwray in #190</li>
203
+ <li>mockhttp - chore: defense - gate staged publishes on Aikido scan-release by @jaredwray in #191</li>
204
+ <li>mockhttp - chore: defense - prefer tilde ranges for runtime deps by @jaredwray in #192</li>
205
+ <li>mockhttp - chore: defense - record repository lockdown by @jaredwray in #193</li>
206
+ <li>root - chore: remove set-cookie-parser override by @jaredwray in #194</li>
207
+ <li>root - chore: remove @ungap/structured-clone override by @jaredwray in #195</li>
208
+ <li>root - chore: remove glob override by @jaredwray in #196</li>
209
+ <li>root - chore: upgrade code quality dependencies by @jaredwray in #197</li>
210
+ <li>root - chore: upgrade TypeScript and build tooling by @jaredwray in #198</li>
211
+ <li>root - chore: upgrade package manager by @jaredwray in #200</li>
212
+ <li>root - chore: upgrade fastify dependencies by @jaredwray in #201</li>
213
+ <li>root - chore: upgrade @scalar/api-reference by @jaredwray in #202</li>
214
+ <li>root - chore: upgrade hookified by @jaredwray in #203</li>
215
+ <li>root - chore: defense - prefer caret ranges over tilde by @jaredwray in #204</li>
216
+ <li>root - chore: upgrade Docker Node.js runtime image by @jaredwray in #205</li>
217
+ </ul>
218
+ <p><strong>Full diff:</strong> <a href="https://github.com/jaredwray/mockhttp/compare/v1.8.1...v1.9.0">https://github.com/jaredwray/mockhttp/compare/v1.8.1...v1.9.0</a></p>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </main>
223
+ </div>
224
+ <footer>
225
+ <a href="/llms.txt" class="footer__link">llms.txt</a>
226
+ <a href="/feed.xml" class="footer__link" aria-label="RSS Feed">
227
+ <svg class="footer__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><circle cx="6.18" cy="17.82" r="2.18"/><path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/></svg>
228
+ </a>
229
+
230
+ <a href="https://docula.org/" target="_blank" rel="noopener noreferrer" class="footer__link" aria-label="Docula">
231
+ <svg class="footer__bat" xmlns="http://www.w3.org/2000/svg" viewBox="70 110 360 205" fill="currentColor"><path d="M420,173c-16.76-18-72.78-36.65-131.59-30.58A160.15,160.15,0,0,0,269,120.55c-3.78,4.52-11.15,9.45-19,9.45s-15.23-4.93-19-9.45a160.15,160.15,0,0,0-19.4,21.83C152.78,136.31,96.76,154.94,80,173c48.86,5.86,79.5,34.09,79.5,72.06,52.37,0,77,28,90.5,59.94C263.54,273,288.13,245,340.5,245,340.5,207,371.14,178.8,420,173Z"/><circle cx="239.19" cy="145.36" r="3.32" fill="#fff"/><circle cx="260.81" cy="145.36" r="3.32" fill="#fff"/></svg>
232
+ </a>
233
+ <span class="footer__updated">updated: 2026-08-19</span>
234
+ </footer>
235
+ <script src="/css/highlight/highlight.min.js"></script>
236
+ <script>
237
+ document.addEventListener('DOMContentLoaded', () => {
238
+ document.querySelectorAll('pre code').forEach(el => { el.textContent = el.textContent.trimEnd(); });
239
+ hljs.highlightAll();
240
+ var kbd = document.querySelector('.search-button__shortcut');
241
+ if (kbd) kbd.textContent = navigator.platform.indexOf('Mac') > -1 ? '⌘K' : 'Ctrl K';
242
+
243
+ // Copy code buttons
244
+ const copyIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
245
+ const checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
246
+ document.querySelectorAll('pre').forEach(function(pre) {
247
+ const btn = document.createElement('button');
248
+ btn.className = 'copy-code-btn';
249
+ btn.innerHTML = copyIcon;
250
+ btn.setAttribute('aria-label', 'Copy code');
251
+ btn.addEventListener('click', function() {
252
+ const code = pre.querySelector('code');
253
+ const text = code ? code.textContent : pre.textContent;
254
+ navigator.clipboard.writeText(text || '').then(function() {
255
+ btn.innerHTML = checkIcon;
256
+ setTimeout(function() { btn.innerHTML = copyIcon; }, 2000);
257
+ });
258
+ });
259
+ pre.appendChild(btn);
260
+ });
261
+ });
262
+ </script>
263
+ <script>
264
+ (function() {
265
+ var themeKey = window.__doculaThemeKey;
266
+ function getMode() {
267
+ return localStorage.getItem(themeKey) || 'light';
268
+ }
269
+
270
+ function resolveTheme(mode) {
271
+ if (mode === 'system') {
272
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
273
+ }
274
+ return mode;
275
+ }
276
+
277
+ var cachedEls = null;
278
+ function getThemeEls() {
279
+ if (!cachedEls) {
280
+ cachedEls = {
281
+ systemIcon: document.querySelector('.theme-button__icon--system'),
282
+ sunIcon: document.querySelector('.theme-button__icon--sun'),
283
+ moonIcon: document.querySelector('.theme-button__icon--moon'),
284
+ toggle: document.getElementById('theme-toggle')
285
+ };
286
+ }
287
+ return cachedEls;
288
+ }
289
+
290
+ function applyTheme(mode) {
291
+ var resolved = resolveTheme(mode);
292
+ if (resolved === 'light') {
293
+ document.documentElement.setAttribute('data-theme', 'light');
294
+ } else {
295
+ document.documentElement.removeAttribute('data-theme');
296
+ }
297
+
298
+ var els = getThemeEls();
299
+ if (els.systemIcon && els.sunIcon && els.moonIcon) {
300
+ els.systemIcon.classList.add('theme-button__icon--hidden');
301
+ els.sunIcon.classList.add('theme-button__icon--hidden');
302
+ els.moonIcon.classList.add('theme-button__icon--hidden');
303
+ if (mode === 'system') {
304
+ els.systemIcon.classList.remove('theme-button__icon--hidden');
305
+ } else if (mode === 'light') {
306
+ els.sunIcon.classList.remove('theme-button__icon--hidden');
307
+ } else {
308
+ els.moonIcon.classList.remove('theme-button__icon--hidden');
309
+ }
310
+ }
311
+
312
+ if (els.toggle) {
313
+ var labels = { system: 'Theme: system (following system)', light: 'Theme: light', dark: 'Theme: dark' };
314
+ els.toggle.setAttribute('aria-label', labels[mode] || 'Toggle theme');
315
+ }
316
+ }
317
+
318
+ function nextMode(current) {
319
+ var cycle = { system: 'light', light: 'dark', dark: 'system' };
320
+ return cycle[current] || 'system';
321
+ }
322
+
323
+ var currentMode = getMode();
324
+
325
+ document.addEventListener('DOMContentLoaded', function() {
326
+ applyTheme(currentMode);
327
+
328
+ var els = getThemeEls();
329
+ if (els.toggle) {
330
+ els.toggle.addEventListener('click', function() {
331
+ currentMode = nextMode(currentMode);
332
+ localStorage.setItem(themeKey, currentMode);
333
+ applyTheme(currentMode);
334
+ });
335
+ }
336
+ });
337
+
338
+ window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function() {
339
+ if (getMode() === 'system') {
340
+ applyTheme('system');
341
+ }
342
+ });
343
+ })();
344
+ </script>
345
+ <script defer>
346
+ document.addEventListener('DOMContentLoaded', () => {
347
+ const menuToggle = document.getElementById('mobile-menu-toggle');
348
+ const mobileSidebar = document.getElementById('mobile-sidebar');
349
+ const backdrop = document.getElementById('sidebar-backdrop');
350
+ if (menuToggle && mobileSidebar) {
351
+ menuToggle.addEventListener('click', () => {
352
+ mobileSidebar.classList.toggle('mobile-sidebar--open');
353
+ if (backdrop) backdrop.classList.toggle('sidebar-backdrop--visible');
354
+ });
355
+ if (backdrop) {
356
+ backdrop.addEventListener('click', () => {
357
+ mobileSidebar.classList.remove('mobile-sidebar--open');
358
+ backdrop.classList.remove('sidebar-backdrop--visible');
359
+ });
360
+ }
361
+ }
362
+
363
+ // Page dropdown toggle (docs pages)
364
+ const dropdownToggle = document.getElementById('page-dropdown-toggle');
365
+ const dropdownList = document.getElementById('page-dropdown-list');
366
+ if (dropdownToggle && dropdownList) {
367
+ dropdownToggle.addEventListener('click', () => {
368
+ dropdownList.classList.toggle('page-dropdown__list--open');
369
+ });
370
+ }
371
+
372
+ // Active sidebar link highlighting
373
+ const sidebarLinks = document.querySelectorAll('.nav-sidebar__item');
374
+ const currentPath = window.location.pathname;
375
+ sidebarLinks.forEach((link) => {
376
+ if (link.getAttribute('href') === currentPath || link.getAttribute('href') === currentPath.replace(/\/$/, '')) {
377
+ link.classList.add('nav-sidebar__item--active');
378
+ }
379
+ });
380
+
381
+ // Sidebar section collapse/expand
382
+ const SIDEBAR_STORAGE_KEY = 'docula:sidebar-sections';
383
+ const collapsibleSections = document.querySelectorAll('.nav-sidebar__section--collapsible');
384
+ if (collapsibleSections.length > 0) {
385
+ let storedSectionState = {};
386
+ try {
387
+ storedSectionState = JSON.parse(localStorage.getItem(SIDEBAR_STORAGE_KEY) || '{}');
388
+ } catch (e) { storedSectionState = {}; }
389
+
390
+ const setSectionOpen = (section, toggle, open) => {
391
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
392
+ section.classList.toggle('nav-sidebar__section--collapsed', !open);
393
+ };
394
+
395
+ collapsibleSections.forEach((section, idx) => {
396
+ const toggle = section.querySelector('.nav-sidebar__toggle');
397
+ const list = section.querySelector('.nav-sidebar__list');
398
+ if (!toggle || !list) return;
399
+ const listId = 'nav-sidebar-section-' + idx;
400
+ list.id = listId;
401
+ toggle.setAttribute('aria-controls', listId);
402
+
403
+ const key = 'section-' + idx;
404
+ const hasStored = Object.prototype.hasOwnProperty.call(storedSectionState, key);
405
+ const isOpen = hasStored ? !!storedSectionState[key] : true;
406
+ setSectionOpen(section, toggle, isOpen);
407
+
408
+ toggle.addEventListener('click', () => {
409
+ const next = toggle.getAttribute('aria-expanded') !== 'true';
410
+ setSectionOpen(section, toggle, next);
411
+ storedSectionState[key] = next;
412
+ try {
413
+ localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(storedSectionState));
414
+ } catch (e) { /* storage unavailable */ }
415
+ });
416
+ });
417
+ }
418
+
419
+ // Active nav link highlighting (header + mobile). Highlight only the
420
+ // single most-specific link: the one whose path is the longest prefix of
421
+ // the current path. A plain startsWith() check lights up every link whose
422
+ // path merely prefixes the current one, so when the site is embedded under
423
+ // a base path (e.g. served at /developer with docs at that root) the
424
+ // Documentation link — whose path is a prefix of every page — would stay
425
+ // highlighted everywhere. Trailing slashes are normalized so matches stop
426
+ // at a path boundary. We read the anchor's resolved origin/pathname so
427
+ // absolute, relative, and same-origin hrefs all work, while cross-origin
428
+ // links and non-anchor items (e.g. the mobile logout button) are skipped.
429
+ const withTrailingSlash = (p) => (p.endsWith('/') ? p : p + '/');
430
+ const here = withTrailingSlash(currentPath);
431
+ const markActiveNav = (selector, activeClass) => {
432
+ let bestLink = null;
433
+ let bestLength = -1;
434
+ document.querySelectorAll(selector).forEach((link) => {
435
+ // Only same-origin anchors can match the current pathname.
436
+ if (!link.href || link.origin !== window.location.origin) return;
437
+ const base = withTrailingSlash(link.pathname);
438
+ if (here.startsWith(base) && base.length > bestLength) {
439
+ bestLink = link;
440
+ bestLength = base.length;
441
+ }
442
+ });
443
+ if (bestLink) bestLink.classList.add(activeClass);
444
+ };
445
+ markActiveNav('.header-bottom__item', 'header-bottom__item--active');
446
+ markActiveNav('.mobile-nav__item', 'mobile-nav__item--active');
447
+
448
+ // TOC extraction: move the inline TOC into the right sidebar and hide the original
449
+ const tocHeading = document.getElementById('table-of-contents');
450
+ if (tocHeading) {
451
+ const tocList = tocHeading.nextElementSibling;
452
+ const tocTarget = document.getElementById('toc-list');
453
+ if (tocList && tocList.tagName === 'UL' && tocTarget) {
454
+ tocTarget.innerHTML = tocList.innerHTML;
455
+ tocHeading.style.display = 'none';
456
+ tocList.style.display = 'none';
457
+ }
458
+ }
459
+
460
+ // Hide the TOC aside if it has no entries
461
+ const tocSidebar = document.getElementById('toc-list');
462
+ if (tocSidebar && tocSidebar.children.length === 0) {
463
+ const aside = tocSidebar.closest('.content-aside');
464
+ if (aside) { aside.style.display = 'none'; }
465
+ }
466
+
467
+ // Image lightbox
468
+ const lightboxOverlay = document.createElement('div');
469
+ lightboxOverlay.className = 'lightbox-overlay';
470
+ lightboxOverlay.addEventListener('click', function(e) {
471
+ if (e.target !== lightboxImg) lightboxOverlay.classList.remove('lightbox-overlay--visible');
472
+ });
473
+ const lightboxClose = document.createElement('button');
474
+ lightboxClose.className = 'lightbox-close';
475
+ lightboxClose.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
476
+ lightboxClose.addEventListener('click', function() {
477
+ lightboxOverlay.classList.remove('lightbox-overlay--visible');
478
+ });
479
+ const lightboxImg = document.createElement('img');
480
+ lightboxOverlay.appendChild(lightboxClose);
481
+ lightboxOverlay.appendChild(lightboxImg);
482
+ document.body.appendChild(lightboxOverlay);
483
+
484
+ document.querySelectorAll('.article__main img, .changelog-entry-body img').forEach(function(img) {
485
+ if (img.closest('a')) return;
486
+ img.addEventListener('click', function() {
487
+ lightboxImg.src = img.src;
488
+ lightboxOverlay.classList.add('lightbox-overlay--visible');
489
+ });
490
+ });
491
+ });
492
+ </script>
493
+ </body>
494
+
495
+ </html>