@aiquants/markdown-explorer 0.2.0 → 0.3.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.
- package/CHANGELOG.md +23 -0
- package/README.md +26 -7
- package/dist/{ExplorerSplit-DB37U7Kh.js → ExplorerSplit-B7pmzR8t.js} +2 -2
- package/dist/{ExplorerSplit-D2plJRRe.cjs → ExplorerSplit-Bi3yU6NG.cjs} +1 -1
- package/dist/{MultiLayout-DCPDu1-l.js → MultiLayout-BxeXOLX1.js} +4 -4
- package/dist/{MultiLayout-UyWxs6Xy.cjs → MultiLayout-DtFrFRmS.cjs} +1 -1
- package/dist/{TreeLayout-CLWbWjOc.cjs → TreeLayout-BQUfVNlA.cjs} +1 -1
- package/dist/{TreeLayout-BoyFelK2.js → TreeLayout-D6hCSihW.js} +3 -3
- package/dist/core/config.d.cts +4 -2
- package/dist/core/config.d.ts +4 -2
- package/dist/{dataRoute-CKbdwkQ7.cjs → dataRoute-B9Wk-G7A.cjs} +1 -1
- package/dist/{dataRoute-Bkfwfp1R.js → dataRoute-zUX-oR9j.js} +39 -38
- package/dist/{index-BfuAu2GV.js → index-BJTmp0lR.js} +3 -3
- package/dist/{index-Cx3-jSna.cjs → index-DkiGlM7C.cjs} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +13 -13
- package/dist/location-DfYTzfFS.cjs +1 -0
- package/dist/{location-fVl2R1ee.js → location-V5BKeWNs.js} +178 -164
- package/dist/sanitizeWorker.cjs +1 -1
- package/dist/sanitizeWorker.js +1 -1
- package/dist/server/anonymous.d.cts +2 -2
- package/dist/server/anonymous.d.ts +2 -2
- package/dist/server/ports.d.cts +4 -1
- package/dist/server/ports.d.ts +4 -1
- package/dist/server.cjs +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +631 -603
- package/docs/behavior/security.md +16 -2
- package/package.json +3 -3
- package/dist/location-Oo21lTyV.cjs +0 -1
|
@@ -80,7 +80,7 @@ In a tree or children listing, an entry with an empty name, or with a path the b
|
|
|
80
80
|
|
|
81
81
|
## Responses
|
|
82
82
|
|
|
83
|
-
- Every JSON data response carries `Content-Type: application/json; charset=utf-8`, `Cache-Control: private, no-store`, `X-Content-Type-Options: nosniff`, `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; sandbox` and `Cross-Origin-Resource-Policy: same-origin`, and has a JSON body.
|
|
83
|
+
- Every JSON data response of the explorer's own carries `Content-Type: application/json; charset=utf-8`, `Cache-Control: private, no-store`, `X-Content-Type-Options: nosniff`, `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; sandbox` and `Cross-Origin-Resource-Policy: same-origin`, and has a JSON body.
|
|
84
84
|
- The page loader's responses carry `Cache-Control: private, no-store` through the exported `headers`, to both the HTML document and the single-fetch data.
|
|
85
85
|
- The data route answers a React Router single-fetch request (`<dataPath>/<operation>.data`: its own URL path ends in `.data`, which React Router strips from the route's `url` argument and `params` but not from `request.url`) with a `not-found` failure (404) before authenticating or calling a source. React Router would run the route for it, read the whole response into memory and keep none of the headers above but `Set-Cookie`.
|
|
86
86
|
- Status codes: 200 for a tree, children, document or refresh answer (a refresh answers the reloaded tree, as a tree request does, failures included), 400 for a missing or malformed parameter, 401 when the reader is signed out, 403 when the reader may not use the explorer (or a refresh, or a document request marked as one's, lacks its proof), 404 for an unknown operation or source and for a children request to a source without `children()`, 405 (with `Allow`) for a wrong method.
|
|
@@ -99,6 +99,20 @@ In a tree or children listing, an entry with an empty name, or with a path the b
|
|
|
99
99
|
A body with a `Content-Length` carries exactly that many bytes: the bytes past it are dropped and the host's stream is cancelled, and a stream that ends short fails the response, so a file that changes while it is served never gets a body that disagrees with its length and ETag.
|
|
100
100
|
- Responses never contain tokens, principals, server paths or raw error messages. A failure carries only its reason and an optional host-defined code (`[a-z0-9-]{1,64}`); everything else goes to the server's `logger`. `new ExplorerError` throws a `RangeError` for a malformed code, and a failure document returned with one is logged and sent as `failed`.
|
|
101
101
|
|
|
102
|
+
- The `link-card` operation passes the answers of `@aiquants/markdown`'s link-card handler through unchanged (see [Link cards](#link-cards)): a card is `200` JSON with `Cache-Control: private, max-age=1800`, a link without a card `204 No Content` with `X-Link-Card-Skip`, and the handler's own errors JSON with `no-store`; each carries `nosniff` and never `Set-Cookie`. Only the explorer's own refusals in front of it (`not-found`, 405, a refused authentication) are the explorer's failure JSON above.
|
|
103
|
+
|
|
104
|
+
## Link cards
|
|
105
|
+
|
|
106
|
+
- **One route, off by default.** Cards exist only with `embeds: { linkCards: true }`; the endpoint is the data route's `link-card` operation (`<dataPath>/link-card?url=<href>`, `GET` and `HEAD`), which `defineExplorerConfig` writes into `config.embeds.linkCardEndpoint`, and no other route serves cards. With link cards off, `link-card` answers exactly as an unknown operation does (`not-found`, whatever the method). The `.data` refusal comes first, then the operation, then the method (405 with `Allow: GET, HEAD`), all before authenticating.
|
|
107
|
+
- **Read-only authentication.** The operation calls `authenticate(request, { readOnly: true })`; every other request passes `{ readOnly: false }`. In read-only mode the host only reads the session: it never refreshes a token, writes no cookie and returns no `headers`. Cards ask in the background and can still be in flight while the reader signs out, and a refreshed session cookie arriving after the sign-out deleted it would sign the reader back in.
|
|
108
|
+
- **Fail closed.** A read-only authentication that carries any header (whatever its status; an empty `headers` holds none) is a broken contract: none of its headers is forwarded, the failure is logged once through `logger` with the operation `authenticate`, and the request is answered `failed` (500, `private, no-store`, no `Set-Cookie`).
|
|
109
|
+
A malformed authentication, or an `authenticate` that throws, is answered the same way; an `ExplorerError` answers its reason without a log, as on every operation, and an aborted request is not logged.
|
|
110
|
+
Nothing logged quotes a header value: `headers` that `Headers` cannot read are refused with a fixed message (on every request, the page loader included), and a `Response` that `authenticate` throws — React Router's `throw redirect(...)`, whose headers carry cookies — is logged by its status alone. `signed-out` and `forbidden` both mean no card: the handler answers its 401 with `no-store` before fetching anything.
|
|
111
|
+
- **SSRF guards live in `@aiquants/markdown`.** The explorer creates `createLinkCardHandler` once, with `authorize` as its only option, and adds no option of its own: internal addresses never become cards in any environment.
|
|
112
|
+
The handler refuses loopback, private, link-local, CGNAT and every other non-global IPv4 or IPv6 address in every form the URL parser normalizes, internal names (`localhost`, single-label names, `.local`, `.internal`, `.home.arpa`), names that resolve to such addresses (checked again at connect time against DNS rebinding) and every redirect hop to them, and it bounds time, redirects, headers and body.
|
|
113
|
+
Its answer for such a link — `204 No Content` with `X-Link-Card-Skip: private-network` — is passed through unchanged, and `@aiquants/markdown`'s link card shows the link as a compact card and writes one `console.info` line; the explorer logs nothing of its own for cards.
|
|
114
|
+
- **Same origin.** The browser asks the page's own origin with its credentials and `redirect: "error"`, so no `connect-src` beyond `'self'` is needed and no cross-origin page can read an answer (the data route grants no CORS).
|
|
115
|
+
|
|
102
116
|
## Caches and identity
|
|
103
117
|
|
|
104
118
|
- The server's tree cache is keyed by the reader's scope (derived from the principal's stable key) and the source. `cacheScope: "shared"` lets readers share one entry when the tree does not depend on who reads it; `createFileSystemSource` uses `"principal"` unless told otherwise, which a source wrapped to filter its tree per reader must keep. Entries expire after `treeCache.ttlMs`, and the cache holds at most `treeCache.maxEntries` trees and `treeCache.maxBytes` of their JSON.
|
|
@@ -111,7 +125,7 @@ In a tree or children listing, an entry with an empty name, or with a path the b
|
|
|
111
125
|
|
|
112
126
|
## What the host must do
|
|
113
127
|
|
|
114
|
-
- Authenticate in `authenticate` and keep `principalKey` stable for one reader.
|
|
128
|
+
- Authenticate in `authenticate` and keep `principalKey` stable for one reader. Honour its `mode`: with `{ readOnly: true }` (the `link-card` operation) only read the session — never refresh a token, write no cookie and return no `headers`; the explorer refuses an authentication that does.
|
|
115
129
|
- Set `scopeSecret` from configuration, kept out of the source code and the browser: at least 32 characters, the same on every server instance.
|
|
116
130
|
- Keep the package external in your server bundle, so the sanitizer's worker file stays next to the server entry.
|
|
117
131
|
- Implement the ports: `document` and `asset` (and `children` for lazy folders) read what the explorer admitted, with the `readPath` it hands them, and report the same `version` for the same file.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiquants/markdown-explorer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Markdown document explorer for React Router 8 (framework mode): a source tree with a single-document view and a multi-panel reading view, storage-agnostic server ports, golden-ratio layout and an en / ja UI.",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@aiquants/directory-tree": "^4.1.0",
|
|
62
62
|
"@aiquants/drag-drop-panels": "^0.9.1",
|
|
63
|
-
"@aiquants/markdown": "^6.
|
|
63
|
+
"@aiquants/markdown": "^6.1.0",
|
|
64
64
|
"@aiquants/resize-panels": "^2.1.0",
|
|
65
65
|
"@aiquants/virtualscroll": "^3.14.0",
|
|
66
66
|
"react": "^19.2.7",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@aiquants/directory-tree": "4.1.0",
|
|
72
72
|
"@aiquants/drag-drop-panels": "0.9.1",
|
|
73
|
-
"@aiquants/markdown": "6.
|
|
73
|
+
"@aiquants/markdown": "6.1.0",
|
|
74
74
|
"@aiquants/resize-panels": "2.1.0",
|
|
75
75
|
"@aiquants/virtualscroll": "3.14.0",
|
|
76
76
|
"@heroicons/react": "^2.2.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";const O=require("@aiquants/markdown/embeds"),h=require("@aiquants/markdown/viewer-settings"),v=require("./dataRoute-CKbdwkQ7.cjs"),C=e=>{if(e===null||typeof e!="object")return!1;const t=Object.getPrototypeOf(e);return t===Object.prototype||t===null},L=new Intl.Collator("en",{numeric:!0,sensitivity:"variant"}),P=Object.freeze({sortMode:"native",directoriesFirst:!1,selectionMode:"single",doubleClickAction:"recursive",expandIconSize:8,alwaysExpanded:!1,removeRootIndent:!1,lines:!0,expandIcons:!0,directoryIcons:!0,fileIcons:!0}),ce=[!0,!1],le=(e,t)=>t?e.settingOn:e.settingOff,E=(e,t)=>({key:e,labelKey:t,values:ce,formatValue:le,requiresItemSelection:!1}),w=Object.freeze([{key:"sortMode",labelKey:"settingSortMode",values:["native","asc","desc"],formatValue:(e,t)=>t==="native"?e.sortNative:t==="asc"?e.sortAscending:e.sortDescending,requiresItemSelection:!1},E("directoriesFirst","settingDirectoriesFirst"),{key:"selectionMode",labelKey:"settingSelectionMode",values:["single","multiple","none"],formatValue:(e,t)=>t==="single"?e.selectionSingle:t==="multiple"?e.selectionMultiple:e.selectionNone,requiresItemSelection:!0},{key:"doubleClickAction",labelKey:"settingDoubleClick",values:["recursive","toggle"],formatValue:(e,t)=>t==="recursive"?e.doubleClickRecursive:e.doubleClickToggle,requiresItemSelection:!1},{key:"expandIconSize",labelKey:"settingExpandIconSize",values:[8,13,5],formatValue:(e,t)=>e.expandIconSizeValue(t),requiresItemSelection:!1},E("alwaysExpanded","settingAlwaysExpanded"),E("removeRootIndent","settingRemoveRootIndent"),E("lines","settingTreeLines"),E("expandIcons","settingExpandIcons"),E("directoryIcons","settingDirectoryIcons"),E("fileIcons","settingFileIcons")]),de=e=>e?w:w.filter(t=>!t.requiresItemSelection),X=e=>{const t=w.find(r=>r.key===e);if(t===void 0)throw new RangeError(`[markdown-explorer] no tree setting descriptor for ${e}`);return t},me=(e,t)=>{const r=X(t).values,n=r[(r.indexOf(e[t])+1)%r.length];return{...e,[t]:n}},ue=(e,t,r)=>X(r).formatValue(e,t[r]),G=2,k=Object.freeze({}),D=(e,t)=>Object.freeze(Object.fromEntries(w.filter(r=>t(r,e[r.key])).map(r=>[r.key,e[r.key]]))),fe=e=>{if(e===null||typeof e!="object"||Array.isArray(e))return k;const t=e;return t.version!==G||t.settings===null||typeof t.settings!="object"||Array.isArray(t.settings)?k:D(t.settings,(r,n)=>r.values.includes(n))},ge=(e,t)=>{const r=D(t,(n,o)=>o!==void 0&&o!==e[n.key]);return Object.keys(r).length===0?e:Object.freeze({...e,...r})},Ee=(e,t,r,n)=>D({...e,[t]:r},(o,i)=>i!==void 0&&!(o.key===t&&i===n[t])),we=e=>JSON.stringify({version:G,settings:e}),Se=e=>{if(e.sortMode==="native"&&!e.directoriesFirst)return;const t=e.sortMode==="desc"?-1:1;return(r,n)=>{if(e.directoriesFirst&&r.type!==n.type)return r.type==="directory"?-1:1;if(e.sortMode==="native")return 0;const o=L.compare(r.name,n.name);return t*(o!==0?o:L.compare(r.path,n.path))}},u=(1+Math.sqrt(5))/2,pe=100/u**3,he=100/u**4,z=100/u**2,W=13,_e=16,ye=Object.freeze({gripThickness:5,gripLength:34,barThickness:2,barLength:21}),Oe=Object.freeze([89,144,233,377]),Te=34,xe=44,be=24,Ne=1/u,Re=1/u**3,Ae="40ch",Ie=e=>{if(!Number.isFinite(e)||e<=0)throw new RangeError(`[markdown-explorer] rootFontPixels must be a finite number > 0; got ${String(e)}`)},J=e=>(Ie(e),W*e),Le=e=>J(e)*u**2,Pe=233,ke=Oe[0],j=(e,t)=>{if(!Number.isFinite(t)||t<0)throw new RangeError(`[markdown-explorer] ${e} must be a finite number >= 0; got ${String(t)}`)},Me=({trackInlineSize:e,gap:t,minColumnInlineSize:r,logicalColumns:n})=>{if(j("trackInlineSize",e),j("gap",t),!Number.isFinite(r)||r<=0)throw new RangeError(`[markdown-explorer] minColumnInlineSize must be a finite number > 0; got ${String(r)}`);if(!Number.isInteger(n)||n<1)throw new RangeError(`[markdown-explorer] logicalColumns must be a positive integer; got ${String(n)}`);const o=Math.floor((e+t)/(r+t));return Math.min(n,Math.max(1,o))},$e=(e,t)=>{const{trackInlineSize:r,gap:n,minColumnInlineSize:o,logicalColumns:i}=t;if(!([r,n,o].every(Number.isFinite)&&r>0&&n>=0&&o>0))return e;const s=Me(t);if(e===null||s===e)return s;const l=Math.min(i,Math.max(1,e));if(s>l){const b=(l+1)*o+l*n+n/u;return r>=b?s:l}return r>=l*o+(l-1)*n?l:s},ve=(e,t)=>{if(!(Number.isInteger(e)&&Number.isInteger(t))||t<1||t>e)throw new RangeError(`[markdown-explorer] visible columns must be an integer in 1..${String(e)}; got ${String(t)}`);return Array.from({length:t},(r,n)=>[Math.floor(n*e/t),Math.floor((n+1)*e/t)])},Ce=31536e3,De=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u,ze=e=>{if(typeof e!="string"||!De.test(e))throw new RangeError(`[markdown-explorer] settingsCookie must be a cookie-name token; got ${JSON.stringify(e)}`)},F=e=>typeof e=="number"&&Number.isFinite(e)&&e>0&&e<=z,je=(e,t)=>{if(e===null||typeof e!="object"||Array.isArray(e))return{};const r=e,{treeExplorer:n,multiExplorer:o,style:i,align:a}=r;return{...F(n)?{treeExplorer:n}:{},...F(o)?{multiExplorer:o}:{},...typeof i=="string"&&t.includes(i)?{style:i}:{},...typeof a=="string"&&h.CONTENT_ALIGNS.includes(a)?{align:a}:{}}},Fe=(e,t)=>{if(e!==null)for(const r of e.split(";")){const n=r.indexOf("=");if(!(n<0)&&r.slice(0,n).trim()===t)return r.slice(n+1).trim()}},Ve=(e,t)=>{if(e===void 0||e==="")return{};try{return je(JSON.parse(decodeURIComponent(e)),t)}catch{return{}}},x=100,Ue=Math.floor(z*x)/x,Xe=e=>{const t=r=>Math.min(Ue,Math.max(1,Math.round(r*x))/x);return encodeURIComponent(JSON.stringify({...e.treeExplorer===void 0?{}:{treeExplorer:t(e.treeExplorer)},...e.multiExplorer===void 0?{}:{multiExplorer:t(e.multiExplorer)},...e.style===void 0?{}:{style:e.style},...e.align===void 0?{}:{align:e.align}}))},Ge=(e,t,r)=>`${e}=${Xe(t)}; Path=/; Max-Age=${String(Ce)}; SameSite=Lax${r?"; Secure":""}`,M=Object.freeze({tree:"tree",multi:"multi",single:"single"}),H=Object.freeze({default:12,min:1,max:64}),q=Object.freeze({default:4,min:1,max:6}),B=Object.freeze({default:500,min:1,max:1e4}),K=Object.freeze({default:16,min:1,max:64}),We=/^\/(?:[A-Za-z0-9._~-]+\/)*[A-Za-z0-9._~-]+$/u,Je=/^[A-Za-z0-9][A-Za-z0-9_-]*$/u,He=/^[a-z0-9][a-z0-9-]*$/u,qe=/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/u,V=(e,t)=>{if(typeof t!="string")throw new TypeError(`[markdown-explorer] ${e} must be a string; got ${typeof t}`);if(!We.test(t)||t.split("/").some(r=>r==="."||r===".."))throw new RangeError(`[markdown-explorer] ${e} must start with "/", have no trailing "/", and use only letters, digits and "._~-" in each segment; got ${JSON.stringify(t)}`)},d=e=>typeof e=="string"?JSON.stringify(e):typeof e=="bigint"?`${String(e)}n`:Object.is(e,-0)?"-0":typeof e=="function"?"a function":Array.isArray(e)?"an array":e!==null&&typeof e=="object"?C(e)?"a plain object":`a non-plain object (${Object.prototype.toString.call(e)})`:String(e),_=(e,t,r)=>{if(t===void 0)return[];if(!C(t))throw new TypeError(`[markdown-explorer] ${e} must be a plain object; got ${d(t)}`);const n=Object.entries(t);for(const[o]of n)if(!r.includes(o))throw new RangeError(`[markdown-explorer] ${e}.${o} is not an option; expected ${r.join(", ")}`);return n.filter(([,o])=>o!==void 0)},Be=e=>{const t={...M};for(const[r,n]of _("views",e,Object.keys(M)))t[r]=n;for(const[r,n]of Object.entries(t))if(typeof n!="string"||!Je.test(n))throw new RangeError(`[markdown-explorer] views.${r} must start with a letter or digit and use only letters, digits, "_" and "-"; got ${JSON.stringify(n)}`);return Object.freeze(t)},T=(e,t,r)=>{if(t===void 0)return r.default;if(typeof t!="number"||!Number.isInteger(t)||t<r.min||t>r.max)throw new RangeError(`[markdown-explorer] ${e} must be an integer in ${String(r.min)}..${String(r.max)}; got ${d(t)}`);return t},Ke=e=>{const t=new Map(_("embeds",e,["providers","linkCardEndpoint"]));if(t.size===0)return O.NO_EMBEDS;const r=t.has("providers")?t.get("providers"):[];if(!Array.isArray(r))throw new RangeError(`[markdown-explorer] embeds.providers must be an array of provider ids; got ${d(r)}`);for(const o of r)if(!O.isEmbedProviderId(o))throw new RangeError(`[markdown-explorer] embeds.providers must hold only ${O.EMBED_PROVIDER_IDS.join(", ")}; got ${d(o)}`);const n=t.has("linkCardEndpoint")?t.get("linkCardEndpoint"):null;if(n!==null&&!O.isLinkCardEndpoint(n))throw new RangeError(`[markdown-explorer] embeds.linkCardEndpoint must start with "/", have no trailing "/", and use only letters, digits and "._~-" in each segment; got ${d(n)}`);return Object.freeze({providers:Object.freeze([...new Set(r)]),linkCardEndpoint:n})},Ze=e=>{const t=new Map(_("defaultTreeSettings",e,w.map(n=>n.key)));if(t.size===0)return P;const r={...P};for(const n of w){if(!t.has(n.key))continue;const o=t.get(n.key),i=n.values;if(!i.includes(o))throw new RangeError(`[markdown-explorer] defaultTreeSettings.${n.key} must be one of ${i.map(d).join(", ")}; got ${d(o)}`);r[n.key]=o}return Object.freeze(r)},Ye=e=>{if(e===null||typeof e!="object")throw new TypeError("[markdown-explorer] the explorer configuration must be an object");V("basePath",e.basePath),V("dataPath",e.dataPath);const t=Be(e.views);if(new Set([t.tree,t.multi,t.single]).size!==3)throw new RangeError(`[markdown-explorer] the view slugs must differ; got ${JSON.stringify(t)}`);if(e.dataPath===e.basePath)throw new RangeError("[markdown-explorer] dataPath must differ from basePath");if(e.dataPath.startsWith(`${e.basePath}/`)){const s=e.dataPath.slice(e.basePath.length+1).split("/")[0];if(s===t.tree||s===t.multi||s===t.single)throw new RangeError(`[markdown-explorer] dataPath must not start with a view slug under basePath; got ${JSON.stringify(e.dataPath)}`)}ze(e.settingsCookie);const r=e.iframeHosts??[];if(!Array.isArray(r)||r.some(s=>typeof s!="string"||!qe.test(s)))throw new RangeError(`[markdown-explorer] iframeHosts must be lowercase host names without scheme, port or path; got ${JSON.stringify(r)}`);const n=Object.fromEntries(_("multiView",e.multiView,["maxPanels","columns"])),o=Object.fromEntries(_("expandAll",e.expandAll,["maxFolders","maxDepth"])),i=e.documentStyles??h.DEFAULT_MARKDOWN_STYLES;if(!Array.isArray(i)||i.length===0)throw new RangeError("[markdown-explorer] documentStyles must be a non-empty array");const a=new Set;for(const s of i){if(s===null||typeof s!="object"||typeof s.name!="string"||s.name.trim()===""||typeof s.className!="string")throw new RangeError("[markdown-explorer] each document style needs a non-blank name and a className string");if(a.has(s.name))throw new RangeError(`[markdown-explorer] document style names must be unique; ${JSON.stringify(s.name)} repeats`);a.add(s.name)}return Object.freeze({basePath:e.basePath,dataPath:e.dataPath,settingsCookie:e.settingsCookie,views:t,multiView:Object.freeze({maxPanels:T("multiView.maxPanels",n.maxPanels,H),columns:T("multiView.columns",n.columns,q)}),expandAll:Object.freeze({maxFolders:T("expandAll.maxFolders",o.maxFolders,B),maxDepth:T("expandAll.maxDepth",o.maxDepth,K)}),documentStyles:Object.freeze(i.map(s=>Object.freeze({name:s.name,className:s.className}))),iframeHosts:Object.freeze([...new Set(r)]),embeds:Ke(e.embeds),defaultTreeSettings:Ze(e.defaultTreeSettings)})},Z=e=>typeof e=="string"&&He.test(e),Qe=(e,t)=>{if(e===null||typeof e!="object")throw new TypeError(`[markdown-explorer] withExplorerSources needs the configuration of defineExplorerConfig; got ${d(e)}`);if(!Array.isArray(t)||t.length===0)throw new RangeError(`[markdown-explorer] sourceIds must be a non-empty array; got ${d(t)}`);for(const r of t)if(!Z(r))throw new RangeError(`[markdown-explorer] each source id must use lowercase letters, digits and "-" and start with a letter or digit; got ${d(r)}`);if(new Set(t).size!==t.length)throw new RangeError(`[markdown-explorer] source ids must be unique; got ${JSON.stringify(t)}`);return Object.freeze({...e,sourceIds:Object.freeze([...t])})},et=e=>e.documentStyles.map(t=>t.name),c=Object.freeze({source:"source",doc:"doc",maximized:"maximized",style:"style",align:"align",showToc:"showToc",showStyle:"showStyle",showAlign:"showAlign",padding:"padding",width:"width",maxWidth:"maxWidth"}),U=".data",Y=256,Q=/^\d+(?:\.\d+)?$/u,$=/^\d+(?:\.\d+)?(?:px|rem|em|ch|vw|%)$/u,ee=Object.freeze(["showToc","showStyle","showAlign"]),tt=e=>{const t=encodeURIComponent(e.replaceAll("%","%25"));return t.endsWith(U)?`${t.slice(0,-U.length)}%2Edata`:t},rt=e=>e.replaceAll("%25","%"),te=e=>{const{sourceIds:t}=e;if(t===void 0)throw new TypeError("[markdown-explorer] the configuration has no source ids; join the server's sources with withExplorerSources(config, sourceIds)");return t},nt=(e,t,r)=>{const n=r.getAll(c.source);if(n.length===0)return t;const[o]=n;return n.length>1||o===void 0||o===t||!e.includes(o)?null:o},ot=(e,t)=>{const r=t.get(c.style),n=t.get(c.align);return{...r!==null&&e.documentStyles.some(o=>o.name===r)?{style:r}:{},...n!==null&&h.CONTENT_ALIGNS.includes(n)?{align:n}:{}}},re=e=>Number.isFinite(e)&&e>=0&&e<=Y&&Q.test(String(e)),it=e=>{const t=e.get(c.padding),r=t!==null&&Q.test(t)?Number(t):Number.NaN,n=e.get(c.width),o=e.get(c.maxWidth);return{...e.get(c.showToc)==="false"?{showToc:!1}:{},...e.get(c.showStyle)==="false"?{showStyle:!1}:{},...e.get(c.showAlign)==="false"?{showAlign:!1}:{},...re(r)?{padding:r}:{},...n!==null&&$.test(n)?{width:n}:{},...o!==null&&$.test(o)?{maxWidth:o}:{}}},ne=(e,t)=>[...new Set(t.filter(v.isAcceptableDocumentPath))].slice(0,e.multiView.maxPanels),st=(e,t,r)=>{for(const n of t)oe(n);if(new Set(t).size!==t.length)throw new RangeError(`[markdown-explorer] multi-view documents must each appear once; got ${String(t.length)} with repeats`);if(t.length>e.multiView.maxPanels)throw new RangeError(`[markdown-explorer] the multi view opens at most ${String(e.multiView.maxPanels)} documents; got ${String(t.length)}`);if(r!==null&&!t.includes(r))throw new RangeError(`[markdown-explorer] the maximized document must be one of the open documents; got ${JSON.stringify(r.slice(0,80))}`)},oe=e=>{if(!v.isAcceptableDocumentPath(e))throw new RangeError(`[markdown-explorer] not an acceptable document path: ${JSON.stringify(String(e).slice(0,80))}`);return e},at=(e,t)=>{if(t.style!==void 0&&!e.documentStyles.some(r=>r.name===t.style))throw new RangeError(`[markdown-explorer] overrides.style must name a configured document style (${e.documentStyles.map(r=>JSON.stringify(r.name)).join(", ")}); got ${JSON.stringify(t.style)}`);if(t.align!==void 0&&!h.CONTENT_ALIGNS.includes(t.align))throw new RangeError(`[markdown-explorer] overrides.align must be one of ${h.CONTENT_ALIGNS.join(", ")}; got ${JSON.stringify(t.align)}`)},ct=e=>{for(const t of ee)if(e[t]!==void 0&&typeof e[t]!="boolean")throw new RangeError(`[markdown-explorer] embed.${t} must be a boolean; got ${JSON.stringify(e[t])}`);if(e.padding!==void 0&&!re(e.padding))throw new RangeError(`[markdown-explorer] embed.padding must be 0..${String(Y)} px in plain decimal digits; got ${String(e.padding)}`);for(const t of["width","maxWidth"]){const r=e[t];if(r!==void 0&&!$.test(r))throw new RangeError(`[markdown-explorer] embed.${t} must be a CSS length in px, rem, em, ch, vw or %; got ${JSON.stringify(r)}`)}},ie=(e,t)=>{const r=new URLSearchParams;let n=`${e.basePath}/${e.views[t.view]}`;if(t.view==="multi"){const i=t.docs??[],a=t.maximized??null;st(e,i,a);for(const s of i)r.append(c.doc,s);a!==null&&r.set(c.maximized,a)}else t.doc!==void 0&&t.doc!==null&&(n+=`/${tt(oe(t.doc))}`);if(t.source!==void 0){const i=te(e);if(!i.includes(t.source))throw new RangeError(`[markdown-explorer] unknown source ${JSON.stringify(t.source)}; expected one of ${i.join(", ")}`);t.source!==i[0]&&r.set(c.source,t.source)}if(t.view!=="multi"&&t.overrides!==void 0&&(at(e,t.overrides),t.overrides.style!==void 0&&r.set(c.style,t.overrides.style),t.overrides.align!==void 0&&r.set(c.align,t.overrides.align)),t.view==="single"&&t.embed!==void 0){const{embed:i}=t;ct(i);for(const a of ee)i[a]===!1&&r.set(c[a],"false");i.padding!==void 0&&r.set(c.padding,String(i.padding)),i.width!==void 0&&r.set(c.width,i.width),i.maxWidth!==void 0&&r.set(c.maxWidth,i.maxWidth)}const o=r.toString();return o===""?n:`${n}?${o}`},lt=(e,t)=>Object.keys(e.views).find(r=>e.views[r]===t),dt=(e,t,r)=>{const n=te(e),[o]=n;if(o===void 0)throw new RangeError("[markdown-explorer] the configuration has no source");const i=nt(n,o,r),a=i===null?o:i,s=m=>({kind:"redirect",href:ie(e,m),location:m}),l=()=>s({view:"tree",source:a,doc:null,overrides:{}});if(t===void 0||t==="")return l();const S=t.indexOf("/"),b=S<0?t:t.slice(0,S),N=S<0?null:t.slice(S+1),f=lt(e,b);if(f===void 0)return l();if(f==="multi"){const m=r.getAll(c.doc),p=ne(e,m),R=r.getAll(c.maximized),[A]=R,I=R.length===1&&A!==void 0&&p.includes(A)?A:null;return N===null&&i!==null&&p.length===m.length&&p.every((se,ae)=>se===m[ae])&&(R.length===0||I!==null)?{kind:"location",location:{view:"multi",source:a,docs:p,maximized:I}}:s({view:"multi",source:a,docs:p,maximized:I})}const g=N===null?null:rt(N);if(g!==null&&!v.isAcceptableDocumentPath(g))return l();const y=ot(e,r);if(f==="single"){if(g===null)return l();const m=it(r);return i===null?s({view:f,source:a,doc:g,overrides:y,embed:m}):{kind:"location",location:{view:f,source:a,doc:g,overrides:y,embed:m}}}return i===null?s({view:f,source:a,doc:g,overrides:y}):{kind:"location",location:{view:f,source:a,doc:g,overrides:y}}};exports.ASSUMED_ROOT_FONT_PIXELS=_e;exports.COARSE_TARGET_PIXELS=xe;exports.COLUMN_MIN_MEASURE=Ae;exports.CONTROL_BLOCK_PIXELS=Te;exports.DEFAULT_TREE_SETTINGS=P;exports.DEFAULT_VIEW_SLUGS=M;exports.EXPAND_ALL_DEPTH_LIMITS=K;exports.EXPAND_ALL_FOLDER_LIMITS=B;exports.EXPLORER_DEFAULT_PERCENT=pe;exports.EXPLORER_MAX_PERCENT=z;exports.EXPLORER_MIN_REM=W;exports.EXPLORER_MULTI_DEFAULT_PERCENT=he;exports.EXPLORER_WIDTH_PERSIST_DELAY_MS=Pe;exports.FINE_TARGET_PIXELS=be;exports.GOLDEN_RATIO=u;exports.MULTI_VIEW_COLUMN_LIMITS=q;exports.MULTI_VIEW_PANEL_LIMITS=H;exports.NAME_COLLATOR=L;exports.NO_TREE_SETTING_CHANGES=k;exports.PANEL_MAX_BLOCK_RATIO=Ne;exports.PANEL_MIN_BLOCK_RATIO=Re;exports.PREFETCH_DWELL_MS=ke;exports.SPLIT_HANDLE_PIXELS=ye;exports.advanceTreeSetting=me;exports.applyTreeSettingChanges=ge;exports.canonicalMultiDocuments=ne;exports.columnGroups=ve;exports.createEntryComparator=Se;exports.defineExplorerConfig=Ye;exports.documentStyleNames=et;exports.explorerHref=ie;exports.explorerMinPixels=J;exports.explorerSheetBelowPixels=Le;exports.formatTreeSettingValue=ue;exports.isExplorerSourceId=Z;exports.isPlainObject=C;exports.parseViewSettings=Ve;exports.readCookieValue=Fe;exports.readTreeSettingChanges=fe;exports.recordTreeSettingChange=Ee;exports.replanVisibleColumns=$e;exports.resolveExplorerLocation=dt;exports.serializeTreeSettingChanges=we;exports.viewSettingsCookieString=Ge;exports.visibleTreeSettingDescriptors=de;exports.withExplorerSources=Qe;
|